实验二-进程管理Word格式文档下载.doc
《实验二-进程管理Word格式文档下载.doc》由会员分享,可在线阅读,更多相关《实验二-进程管理Word格式文档下载.doc(39页珍藏版)》请在冰点文库上搜索。
,getpid());
}
这个程序的定义里并没有包含头文件sys/types.h,这是因为我们在程序中没有用到pid_t类型,pid_t类型即为进程ID的类型。
事实上,在i386架构上(就是我们一般PC计算机的架构),pid_t类型是和int类型完全兼容的,我们可以用处理整形数的方法去处理pid_t类型的数据,比如,用"
%d"
把它打印出来。
编译并运行程序getpid_test.c:
$gccgetpid_test.c-ogetpid_test
$./getpid_test
ThecurrentprocessIDis1980
(你自己的运行结果很可能与这个数字不一样,这是很正常的。
)
再运行一遍:
ThecurrentprocessIDis1981
正如我们所见,尽管是同一个应用程序,每一次运行的时候,所分配的进程标识符都不相同。
2、fork
在2.4.4版内核中,fork是第2号系统调用,其在Linux函数库中的原型是:
pid_tfork(void);
创建一个新进程。
系统调用格式:
pid=fork()
fork()返回值意义如下:
0:
在子进程中,pid变量保存的fork()返回值为0,表示当前进程是子进程。
>
在父进程中,pid变量保存的fork()返回值为子进程的id值(进程唯一标识符)。
-1:
创建失败。
如果fork()调用成功,它向父进程返回子进程的PID,并向子进程返回0,即fork()被调用了一次,但返回了两次。
此时OS在内存中建立一个新进程,所建的新进程是调用fork()父进程(parentprocess)的副本,称为子进程(childprocess)。
子进程继承了父进程的许多特性,并具有与父进程完全相同的用户级上下文。
父进程与子进程并发执行。
核心为fork()完成以下操作:
(1)为新进程分配一进程表项和进程标识符
进入fork()后,核心检查系统是否有足够的资源来建立一个新进程。
若资源不足,则fork()系统调用失败;
否则,核心为新进程分配一进程表项和唯一的进程标识符。
(2)检查同时运行的进程数目
超过预先规定的最大数目时,fork()系统调用失败。
(3)拷贝进程表项中的数据
将父进程的当前目录和所有已打开的数据拷贝到子进程表项中,并置进程的状态为“创建”状态。
(4)子进程继承父进程的所有文件
对父进程当前目录和所有已打开的文件表项中的引用计数加1。
(5)为子进程创建进程上、下文
进程创建结束,设子进程状态为“内存中就绪”并返回子进程的标识符。
(6)子进程执行
虽然父进程与子进程程序完全相同,但每个进程都有自己的程序计数器PC(注意子进程的PC开始位置),然后根据pid变量保存的fork()返回值的不同,执行了不同的分支语句。
例:
…..
pid=fork();
if(pid==0)
printf("
I'
mthechildprocess!
\n"
);
elseif(pid>
0)
mtheparentprocess!
\n"
else
printf("
Forkfail!
……
PC
fork()调用前
fork()调用后
if(pid==0)
\n"
3、exit
在2.4.4版内核中,exit是第1号调用,其在Linux函数库中的原型是:
stdlib.h>
voidexit(intstatus);
不像fork那么难理解,从exit的名字就能看出,这个系统调用是用来终止一个进程的。
无论在程序中的什么位置,只要执行到exit系统调用,进程就会停止剩下的所有操作,清除包括PCB在内的各种数据结构,并终止本进程的运行。
请看下面的程序:
/*exit_test1.c*/
thisprocesswillexit!
exit(0);
neverbedisplayed!
编译后运行:
$gccexit_test1.c-oexit_test1
$./exit_test1
我们可以看到,程序并没有打印后面的"
,因为在此之前,在执行到exit(0)时,进程就已经终止了。
exit系统调用带有一个整数类型的参数status,我们可以利用这个参数传递进程结束时的状态,比如说,该进程是正常结束的,还是出现某种意外而结束的,一般来说,0表示没有意外的正常结束;
其他的数值表示出现了错误,进程非正常结束。
我们在实际编程时,可以用wait系统调用接收子进程的返回值,从而针对不同的情况进行不同的处理。
exit和_exit
_exit在Linux函数库中的原型是:
void_exit(intstatus);
_exit()函数的作用最为简单:
直接使进程停止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构;
exit()函数则在这些基础上作了一些包装,在执行退出之前加了若干道工序,也是因为这个原因,有些人认为exit已经不能算是纯粹的系统调用。
exit()函数与_exit()函数最大的区别就在于exit()函数在调用exit系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件。
在Linux的标准函数库中,有一套称作“高级I/O”的函数,我们熟知的printf()、fopen()、fread()、fwrite()都在此列,它们也被称作“缓冲I/O(bufferedI/O)”,其特征是对应每一个打开的文件,在内存中都有一片缓冲区,每次读文件时,会多读出若干条记录,这样下次读文件时就可以直接从内存的缓冲区中读取,每次写文件的时候,也仅仅是写入内存中的缓冲区,等满足了一定的条件(达到一定数量,或遇到特定字符,如换行符\n和文件结束符EOF),再将缓冲区中的内容一次性写入文件,这样就大大增加了文件读写的速度,但也为我们编程带来了一点点麻烦。
如果有一些数据,我们认为已经写入了文件,实际上因为没有满足特定的条件,它们还只是保存在缓冲区内,这时我们用_exit()函数直接将进程关闭,缓冲区中的数据就会丢失,反之,如果想保证数据的完整性,就一定要使用exit()函数。
请看以下例程:
/*exit2.c*/
outputbegin\n"
contentinbuffer"
编译并运行:
$gccexit2.c-oexit2
$./exit2
outputbegin
contentinbuffer
/*_exit1.c*/
_exit(0);
$gcc_exit1.c-o_exit1
$./_exit1
在一个进程调用了exit之后,该进程并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构。
在Linux进程的5种状态中,僵尸进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集。
/*zombie.c*/
#include<
pid_tpid;
pid=fork();
if(pid<
0) /*如果出错*/
printf("
erroroccurred!
elseif(pid==0)/*如果是子进程*/
exit(0);
else /*如果是父进程*/
sleep(60);
/*休眠60秒,这段时间里,父进程什么也干不了*/
wait(NULL);
/*收集僵尸进程*/
sleep的作用是让进程休眠指定的秒数,在这60秒内,子进程已经退出,而父进程正忙着睡觉,不可能对它进行收集,这样,我们就能保持子进程60秒的僵尸状态。
编译这个程序:
$gcczombie.c-ozombie
后台运行程序,以使我们能够执行下一条命令
$./zombie&
[1]1577
列一下系统内的进程
$ps-ax
......
1177pts/0S0:
00-bash
1577pts/0S0:
00./zombie
1578pts/0Z0:
00[zombie<
defunct>
]
1579pts/0R0:
00ps-ax
系统调用exit,它的作用是使进程退出,但也仅仅限于将一个正常的进程变成一个僵尸进程,并不能将其完全销毁。
僵尸进程虽然对其他进程几乎没有什么影响,不占用CPU时间,消耗的内存也几乎可以忽略不计,但有它在那里呆着,还是让人觉得心里很不舒服。
而且Linux系统中进程数目是有限制的,在一些特殊的情况下,如果存在太多的僵尸进程,也会影响到新进程的产生。
那么,我们该如何来消灭这些僵尸进程呢?
僵尸进程的概念是从UNIX上继承来的。
僵尸进程中保存着很多对程序员和系统管理员非常重要的信息,首先,这个进程是怎么死亡的?
是正常退出呢,还是出现了错误,还是被其它进程强迫退出的?
其次,这个进程占用的总系统CPU时间和总用户CPU时间分别是多少?
发生页错误的数目和收到信号的数目。
这些信息都被存储在僵尸进程中,试想如果没有僵尸进程,进程一退出,所有与之相关的信息都立刻归于无形,而此时程序员或系统管理员需要用到,就不行了。
如何收集这些信息,并终结这些僵尸进程呢?
就要靠waitpid调用和wait调用。
这两者的作用都是收集僵尸进程留下的信息,同时使这个进程彻底消失。
4、wait和waitpid
wait的函数原型是:
#include<
sys/wait.h>
pid_twait(int*status)
进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;
如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。
但如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,我们就可以设定这个参数为NULL,就象下面这样:
pid=wait(NULL);
如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1。
/*wait1.c*/
pid_tpc,pr;
pc=fork();
if(pc<
0) /*如果出错*/
elseif(pc==0){ /*如果是子进程*/
Thisischildprocesswithpidof%d\n"
sleep(10);
/*睡眠10秒钟*/
}
else{ /*如果是父进程*/
pr=wait(NULL);
/*在这里等待*/
Icatchedachildprocesswithpidof%d\n"
),pr);
}
编译并运行:
$ccwait1.c-owait1
$./wait1
Thisischildprocesswithpidof1508
Icatchedachildprocesswithpidof1508
5、waitpid
waitpid系统调用在Linux函数库中的原型是:
pid_twaitpid(pid_tpid,int*status,intoptions)
从本质上讲,系统调用waitpid和wait的作用是完全相同的,但waitpid多出了两个可由用户控制的参数pid和options:
pid
从参数的名字pid和类型pid_t中就可以看出,这里需要的是一个进程ID。
但当pid取不同的值时,在这里有不同的意义。
pid>
0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。
pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。
options
options提供了一些额外的选项来控制waitpid,目前在Linux中只支持WNOHANG和WUNTRACED两个选项,这是两个常数,可以用"
|"
运算符把它们连接起来使用,比如:
ret=waitpid(-1,NULL,WNOHANG|WUNTRACED);
如果我们不想使用它们,也可以把options设为0,如:
ret=waitpid(-1,NULL,0);
如果使用了WNOHANG参数调用waitpid,即使没有子进程退出,它也会立即返回,不会像wait那样永远等下去。
而WUNTRACED参数,由于涉及到一些跟踪调试方面的知识,加之极少用到,这里就不多费笔墨了。
返回值和错误
waitpid的返回值比wait稍微复杂一些,一共有3种情况:
当正常返回的时候,waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1;
当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回
/*waitpid.c*/
pid_tpc,pr;
0) /*如果fork出错*/
Erroroccurredonforking.\n"
elseif(pc==0){ /*如果是子进程*/
/*睡眠10秒*/
/*如果是父进程*/
do{
pr=waitpid(pc,NULL,WNOHANG);
/*使用了WNOHANG参数,waitpid不会在这里等待*/
if(pr==0){ /*如果没有收集到子进程*/
printf("
Nochildexited\n"
sleep
(1);
}
}while(pr==0);
/*没有收集到子进程,就回去继续尝试*/
if(pr==pc)
successfullygetchild%d\n"
pr);
else
someerroroccurred\n"
$ccwaitpid.c-owaitpid
$./waitpid
Nochildexited
successfullygetchild1526
父进程经过10次失败的尝试之后,终于收集到了退出的子进程。
因为这只是一个例子程序,不便写得太复杂,所以我们就让父进程和子进程分别睡眠了10秒钟和1秒钟,代表它们分别作了10秒钟和1秒钟的工作。
父子进程都有工作要做,父进程利用工作的简短间歇察看子进程的是否退出,如退出就收集它。
6、exec
说是exec系统调用,实际上在Linux中,并不存在一个exec()的函数形式,exec指的是一组函数,一共有6个,分别是:
intexecl(constchar*path,constchar*arg,...);
intexeclp(constchar*file,constchar*arg,...);
intexecle(constchar*path,constchar*arg,...,char*constenvp[]);
intexecv(constchar*path,char*constargv[]);
intexecvp(constchar*file,char*constargv[]);
intexecve(constchar*path,char*constargv[],char*constenvp[]);
其中只有execve是真正意义上的系统调用,其它都是在此基础上经过包装的库函数。
exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。
这里的可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。
与一般情况不同,exec函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样,看上去还是旧的躯壳,却已经注入了新的灵魂。
只有调用失败了,它们才会返回一个-1,从原程序的调用点接着往下执行。
每当有进程认为自己不能为系统和用户做出任何贡献了,他就调用任何一个exec,让自己以新的面貌重生;
或者,更普遍的情况是,如果一个进程想执行另一个程序,它就可以fork出一个新进程,然后调用任何一个exec,这样看起来就好像通过执行应用程序而产生了一个新进程一样。
事实上第二种情况被应用得如此普遍,以至于Linux专门为其作了优化,我们已经知道,fork会将调用进程的所有内容原封不动的拷贝到新产生的子进程中去,这些拷贝的动作很消耗时间,而如果fork完之后我们马上就调用exec,这些辛辛苦苦拷贝来的东西又会被立刻抹掉,这看起来非常不划算,于是人们设计了一种"
写时拷贝(copy-on-write)"
技术,使得fork结束后并不立刻复制父进程的内容,而是到了真正使用的时候才复制,这样如果下一条语句是exec,它就不会白白作无用功了,也就提高了效率。
上面6条函数看起来似乎很复杂,但实际上无论是作用还是用法都非常相似,只有很微小的差别。
在学习它们之前,先来了解一下我们习以为常的main函数。
intmain(intargc,char*argv[],char*envp[])
参数argc指出了运行该程序时命令行参数的个数,数组argv存放了所有的命令行参数,数组envp存