|
|
This document is available in: English Castellano ChineseGB Deutsch Francais Italiano Nederlands Russian Turkce Polish |
by Leonardo Giordani <leo.giordani(at)libero.it> 关于作者: 意大利米兰理工学院无线电工程系学生,网络管理员,热衷于编程(主要使用汇编和C/C++语言),1999年以后基本上只在Linux/Unix下工作。
|
摘要:
本系列文章着眼于向读者介绍多任务处理(multitasking)概念及其在Linux系统中的实施。首先阐述多任务处理的理论基础,做后讲解一个使用简单高效的通讯协议来演示进程间通讯的完整程序。所需背景知识:
交错执行程序会给操作系统带来显著的复杂化。为了防止程序运行时互相冲突,必然要将程序及其运行时所需的所有信息封装起来。
在探讨Linux系统内部细节前,我们先定义一些术语:给定一个运行的程序(PROGRAM), 在某一时刻,代码(CODE)是组成程序的指令,内存空间(MEMORY SPACE)是被程序数据占用的部分内存,处理器状态(PROCESSOR STATUS)是微处理器的参数,例如标记(flag)和程序计数器(Program Counter,下一条要被执行的指令的地址)。
我们定义运行态程序(RUNNING PROGRAM)为代码,内存空间,处理器状态等对象的集合。在计算机运行的某一时刻,我们保存一个运行态程序的数据,调用另一运行态程序的数据并执行,之后再让此程序从先前中断处继续执行,如此实现先前所述的交错执行。进程(PROCESS)或任务(TASK)用来描述此类运行态程序。
我们解释一下简介中提到的工作站的运行机制:在任一时刻都只有一个任务在执行(单处理器不能同时做两件事),计算机执行它的代码,在持续一时间段(QUANTUM)后将其挂起。之后保存它的数据并加载另一等候进程的数据,执行一时间段后切换。这就是所谓的多任务处理。
以上所述的多任务处理带来许多问题需要解决,其中大部分是不能被忽视的,例如等待进程队列的管理(SCHEDULING)。然而这是操作系统设计时要考虑的问题,我们会在以后的文章中讨论,也许也会讨论一些Linux内核代码。
能够显示计算机中运行进程信息的命令是ps(1),它是“进程状态(process status)”的缩写。打开shell窗口并输入ps,可以得到以下输出:
PID TTY TIME CMD 2241 ttyp4 00:00:00 bash 2346 ttyp4 00:00:00 ps
我们先声明此输出结果并不完整,但还是可以说明问题:ps列给出了所有从当前终端运行的进程。最后一列给出了运行此进程的命令(例如用mozilla运行Mozilla Web浏览器,用gcc运行GNU编译器)。显然ps应该出现在输出中,这是因为ps列出正在运行的进程时其本身也正在运行。输出的另一进程是Bourne Again Shell,是运行在此终端上的shell。
我们暂不考虑TIME列和TTY列,先讨论PID(Process IDentifier,进程标志符)列。pid是分配给每一进程的正整数(非零,不重复),在进程结束后可以重复使用,在进程运行期间保持不变。所以在你的计算机上运行ps得到的结果和上面的例子可能不会一样。我们打开另外一个shell窗口并输入ps,同时并不关闭前一shell窗口。我们得到同样的输出项,但是进程标志符不一样,证明了即使是相同的程序,也可以是不同的进程。
我们可以列出Linux系统中运行的所有进程:Linux手册页中提到ps的-e选项意谓着“选择所有进程”。 在shell窗口输入“ps -e”,可以得到和上面例子相同格式的长长的进程列表。为了方便分析结果,我们将ps的输出重定向到ps.log文件中:
ps -e > ps.log
现在我们可以使用我们喜欢的编辑器阅读和编辑这个文件(或者仅仅使用less命令)。如文章开始所述,正在运行的进程数量比我们预期的多。在此指出输出结果不但包括我们启动的进程(通过命令行或图形环境启动),还包括其他一些进程,其中有些名字很奇怪。输出的进程数量和进程名称由你的系统配置决定,但是其中有一些共同点。首先,不管你如何配置系统,pid为1的进程必然是init进程,它是所有其它进程的父进程。init进程永远是操作系统中最先执行的进程,因而拥有的pid为1。另一点需要指出的是,输出结果中有许多名字以d结尾的进程,它们被称为守护进程(daemon),是系统中最重要的一些进程。有关init进程和守护进程的细节将在以后的文章中讨论。
我们现在阐述了进程的概念和它在系统中的重要性,下面将介绍一些多任务处理程序的代码:从简单的进程同时执行到并行进程间通信和同步(synchronization)。我们将讨论进程间通信和同步的两个成熟的解决方案:消息(message)和信号量(semaphore)。介绍完消息概念后将在此基础上开发我们自己的应用程序,信号量将在以后的文章中和线程(thread)一起详加讨论。
C语言标准库函数(libc,在Linux中由glibc实施)使用Unix System V的多任务处理机制。Unix System V(现在称为SysV)是一个商业化的Unix实施版本,是两个最重要的Unix家族之一的基础。另一个Unix家族的基础是BSD Unix。
在libc中数据类型pid_t被定义为能够表达pid的整数。从现在开始我们用pid_t类型表示pid的值,这样会更清楚一些,虽然用整型可以同样表达pid的值。
以下函数返回调用此函数的进程的pid。
pid_t getpid (void)
(pid_t在unistd.h和sys/types.h中定义)我们编写一个程序,用来输出它自己的pid。在你喜欢用的编辑器中键入以下代码:
#include <unistd.h> #include <sys/types.h> #include <stdio.h> int main() { pid_t pid; pid = getpid(); printf("The pid assigned to the process is %d\n", pid); return 0; }将程序存为print_pid.c并编译:
gcc -Wall -o print_pid print_pid.c此命令生成一个名为print_pid的可执行程序。如果当前目录没有包括在path环境变量中,需要用“./print_pid”命令来执行此程序。执行结果并不会让我们感到意外:输出一个正整数。如果执行多次,输出结果会逐渐增大。增大值并不固定,因为其它程序可能在两次print_pid执行间运行。例如,试验在两次print_pid执行间运行ps。
现在讨论如何创建进程,首先描述一下创建的过程。当一个程序(进程A)创建另外一个进程B时,两个进程是相同的。它们有相同的代码,内存空间的数据相同(不同的内存空间),处理器状态相同,但之后可能有不同的执行流程。例如根据用户输入或某些随机数据的不同而不同。进程A是父进程而进程B是子进程,现在我们可以更好的了解为什么init进程是所有其它进程的父进程。创建新进程的函数为:
pid_t fork(void)它的名字来源于它使进程的执行路径分叉的特性,返回值是pid。值得注意的是,当前进程复制自己生成父进程和子进程,和其它进程一起交错执行,完成不同的工作。当复制刚刚完成时哪一个进程会被执行,父进程还是子进程?答案很简单:二者之一。(译者注:也可能二者都不执行)操作系统调度程序决定执行哪个进程,它并不关心等待执行的进程是父进程还是子进程,调度算法由其它参数决定。
知道哪个进程在执行是重要的,因为它们的代码相同。两个进程都包含父进程和子进程的代码,但是每个进程都只执行其中的一部分。为了澄清此概念,我们考虑如下算法:
- FORK - IF YOU ARE THE SON EXECUTE (...) - IF YOU ARE THE FATHER EXECUTE (...)以上算法由伪码表达,基于以下事实:fork函数在子进程中返回0,在父进程中返回子进程的pid。所以根据fork函数返回的pid是否为0可以有效地判断下面需要执行哪部分代码。将此算法放入C语言程序中:
int main() { pid_t pid; pid = fork(); if (pid == 0) { CODE OF THE SON PROCESS } CODE OF THE FATHER PROCESS }现在我们编写第一个真正的多任务处理程序。以下代码可以存为fork_demo.c并按前述方法编译,代码中的行号可以方便阅读,但在编译时要删除。此程序创建一个子进程,然后父进程和子进程分别输出一些字符。如果没有出错的话,父进程和子进程应该交错输出。
(01) #include <unistd.h> (02) #include <sys/types.h> (03) #include <stdio.h> (04) int main() (05) { (05) pid_t pid; (06) int i; (07) pid = fork(); (08) if (pid == 0){ (09) for (i = 0; i < 8; i++){ (10) printf("-SON-\n"); (11) } (12) return(0); (13) } (14) for (i = 0; i < 8; i++){ (15) printf("+FATHER+\n"); (16) } (17) return(0); (18) }
(01)-(03)定义必要的头文件(标准I/O,多任务处理)。
main函数(GNU常规做法)在程序正常结束,没有错误的情况下返回0,如果出错则返回错误(05)定义pid类型,(06)定义循环控制变量。这两种类型其实是相同的,但在此为了让程序更清楚而区分开来。
(07)调用fork函数,在子进程中返回0,在父进程中返回子进程pid,返回值在(08)中测试。子进程执行(09)-(13),父进程执行(14)-(16)。
两部分代码根据是父进程还是子进程分别输出8次字符串“-SON-”和“+FATHER+”,然后返回0结束。返回0结束对子进程很重要,因为子进程循环结束后如果不返回,会继续执行父进程的代码(可以试验一下,对计算机无害,只是无法正确完成我们所期望的功能)。因为多任务处理程序(尤其是复杂程序)的多次运行结果不一定相同,所以这类错误不容易被发现,因而根据结果调试程序通常不太可能。
你可能对执行结果并不满意:两个进程并不一定严格地交错输出结果。这是因为两个循环的执行速度可能不同。你的输出结果可能是一组“+FATHER+”字符串接着一个“-SON-"字符串或者正好相反。试着执行多次程序你会发现输出结果会变化。
在printf函数前插入一个随机的延迟会使输出结果看起来更有多任务处理效果:我们用sleep和rand函数来实现。
sleep(rand()%4)这会使进程挂起0到3之间的一个随机秒数(%返回整数除法的余数)。现在代码变为:
(09) for (i = 0; i < 8; i++){ (->) sleep (rand()%4); (10) printf("-FIGLIO-\n"); (11) }父进程中也做同样的修改,存为fork_demo2.c,然后编译执行。程序的执行速度变慢,但我们可以观察到输出顺序的改变。
[leo@mobile ipc2]$ ./fork_demo2 -SON- +FATHER+ +FATHER+ -SON- -SON- +FATHER+ +FATHER+ -SON- -FIGLIO- +FATHER+ +FATHER+ -SON- -SON- -SON- +FATHER+ +FATHER+ [leo@mobile ipc2]$
现在我们面临一个新的问题:在一个并行处理环境中父进程可以创建一定数量的子进程,子进程执行的操作和父进程执行的操作不同。父进程经常需要和子进程通信或至少要和子进程同步(synchronize)以在正确的时间执行特定操作。进程间同步的第一种解决方法是调用wait函数。(译者注:synchronization通常指进程或线程间防止同时读写相同变量或系统资源的机制,在此作者指父进程需知道子进程结束的时间)
pid_t waitpid (pid_t PID, int *STATUS_PTR, int OPTIONS)PID是我们等待结束的进程的pid,STATUS_PTR是指向子进程状态的整形指针(如不需要此信息可置为NULL),OPTIONS是一些选项,我们现在不需要考虑。下面是一个父进程创建子进程并等待其结束的例子:
#include <unistd.h> #include <sys/types.h> #include <stdio.h> int main() { pid_t pid; int i; pid = fork(); if (pid == 0){ for (i = 0; i < 14; i++){ sleep (rand()%4); printf("-SON-\n"); } return 0; } sleep (rand()%4); printf("+FATHER+ Waiting for son's termination...\n"); waitpid (pid, NULL, 0); printf("+FATHER+ ...ended\n"); return 0; }父进程已加入sleep函数的调用,可使输出交错进行。将此程序存为fork_demo3.c,编译执行。这样我们就完成了第一个同步的多任务处理程序。
下篇文章中我们将讨论更多的进程间通信和同步问题。你们可以用上面介绍的函数编写程序并发给我,这样我可以用其中的一些作为优秀解答和错误实例。请发送带注释的C文件和包含程序说明,你的名字,email地址的说明文件给我。
|
主页由LinuxFocus编辑组维护
© Leonardo Giordani, FDL LinuxFocus.org 点击这里向LinuxFocus报告错误或提出意见 |
翻译信息:
|
2003-04-06, generated by lfparser version 2.25