您的位置:首页 > 运维架构 > Linux

Linux进程控制及守护进程

2011-10-23 16:38 176 查看
原文:/article/4716073.html



进程是程序的一次执行,是运行在自己的虚拟地址空间的一个具有独立功能的程序.进程是分配和释放资源的基本单位,当程序执行时,系统创建进程,分配内存和CPU等资源;进程结束时,系统回收这些资源。进程由PCB(进程控制块)来描述:

进程id。系统中每个进程有唯一的id,在C语言中用
pid_t
类型表示,其实就是一个非负整数。

进程的状态,有运行、挂起、停止、僵尸等状态。

进程切换时需要保存和恢复的一些CPU寄存器。

描述虚拟地址空间的信息。

描述控制终端的信息。

当前工作目录(CurrentWorkingDirectory)。

umask
掩码。

文件描述符表,包含很多指向
file
结构体的指针。

和信号相关的信息。

用户id和组id。

控制终端、Session和进程组。

进程可以使用的资源上限(ResourceLimit)。


线程与进程

线程又名轻负荷进程,它是在进程基础上程序的一次执行,一个进程可以拥有多个线程.
线程没有独立的资源,它共享进程的ID,共享进程的资源.
线程是UNIX中最小的调度单位,目前有系统级调度和进程级调度两种线程调度实行方式:系统级调度的操作系统以线程为单位进行调度;进程级调度的操作系统仍以进程为单位进行调度,进程再为其上运行的线程提供调度控制.

守护进程:常驻后台执行的特殊进程,如sysprocinit

读取PID号:getpidgetpgrpgetppid<unistd.h><sys/types.h>

读取用户标识号:getuidgeteuidgetgidgetegid

例子:

#include<unistd.h>

voidmain()

{

printf("pid=[%d],gid=[%d],ppid=[%d]\n",getpid(),getpgrp(),getppid());

printf("uid=[%d],euid=[%d],gid=[%d],egid=[%d]\n",getuid(),geteuid(),getgid(),getegid());

}

#./id1

pid=[3311],gid=[3311],ppid=[2925]

uid=[0],euid=[0],gid=[0],egid=[0]


环境变量

UNIX中,存储了一系列的变量,在shell下执行'env'命令,就可以得到环境变量列表.

环境变量分为系统环境变量和用户环境变量两种.系统环境变量在注册时自动设置,大部分具有特定

的含义;用户环境变量在Shell中使用赋值命令和export命令设置.如下例先设置了变量XYZ,再将其转化

为用户环境变量:

[bill@billstoneUnix_study]$XYZ=/home/bill

[bill@billstoneUnix_study]$env|grepXYZ

[bill@billstoneUnix_study]$exportXYZ

[bill@billstoneUnix_study]$env|grepXYZ

XYZ=/home/bill

[bill@billstoneUnix_study]$

UNIX下C程序中有两种获取环境变量值的方法:全局变量法和函数调用法


(a)全局变量法

UNIX系统中采用一个指针数组来存储全部环境值:

Externchar**environ;

该法常用于将environ作为参数传递的语句中,比如后面提到的execve函数等.

1:#include<stdio.h>

2:

3:externchar**environ;

4:

5:intmain()

6:

7:{

8:

9:char**p=environ;

10:

11:while(*p){

12:

13:fprintf(stderr,"%s\n",*p);

14:

15:p++;

16:

17:}

18:

19:return0;

20:

21:}



(b)函数调用法

UNIX环境下操作环境变量的函数如下:
#include<stdlib.h>
char*getenv(constchar*name);
intsetenv(constchar*name,constchar*value,intrewrite);
voidunsetenv(constchar*name);


函数getenv以字符串形式返回环境变量name的取值,因此每次只能获取一个环境变量的值;而且要使用该函数,必须知道要获取环境变量的名字.



在进程中执行新程序的三种方法

进程和人类一样,都有创建,发展,休眠和死亡等各种生命形态.

函数fork创建新进程,
函数exec执行新程序,
函数sleep休眠进程,
函数wait同步进程和函数
exit结束进程.

创建子进程的两个用途:1.复制代码2.执行新程序


(1)fork-exec

调用fork创建的子进程,将共享父进程的代码空间,复制父进程数据空间,如堆栈等.调用exec族函数将使用新程序的代码覆盖进程中原来的程序代码,并使进程使用函数提供的命令行参数和环境变量去执行

新的程序.
#include<sys/types.h>
#include<unistd.h>
pid_tfork(void);



fork
函数的特点概括起来就是“调用一次,返回两次”,在父进程中调用一次,在父进程和子进程中各返回一次。
一开始是一个控制流程,调用
fork
之后发生了分叉,变成两个控制流程,这也就是“fork”(分叉)这个名字的由来了。子进程中
fork
的返回值是0,而父进程中
fork
的返回值则是子进程的id(从根本上说
fork
是从内核返回的,内核自有办法让父进程和子进程返回不同的值),这样当
fork
函数返回后,程序员可以根据返回值的不同让父进程和子进程执行不同的代码。
fork
的返回值这样规定是有道理的。
fork
在子进程中返回0,子进程仍可以调用
getpid
函数得到自己的进程id,也可以调用
getppid
函数得到父进程的id。在父进程中用
getpid
可以得到自己的进程id,然而要想得到子进程的id,只有将
fork
的返回值记录下来,别无它法。

fork
的另一个特性是所有由父进程打开的描述符都被复制到子进程中
。父、子进程中相同编号的文件描述符在内核中指向同一个
file
结构体,也就是说,
file
结构体的引用计数要增加。

exec函数族有六个函数如下:

#include<unistd.h>

intexecl(constchar*path,constchar*arg0,...,(char*)0);

intexecle(constchar*path,constchar*arg0,...,(char*)0,char*constenvp[]);

intexeclp(constchar*file,constchar*arg0,...,(char*)0);

intexecv(constchar*path,constchar*argv[]);

intexecve(constchar*path,constchar*argv[],constchar*envp[]);

intexecvp(constchar*file,constchar*argv[]);

externchar**environ;

这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回,如果调用出错则返回-1,所以
exec
函数只有出错的返回值而没有成功的返回值。

这些函数原型看起来很容易混,但只要掌握了规律就很好记。不带字母p(表示path)的
exec
函数第一个参数必须是程序的相对路径或绝对路径
,例如
"/bin/ls"
"./a.out"
,而不能是
"ls"
"a.out"
。对于带字母p的函数:

如果参数中包含/,则将其视为路径名。

否则视为不带路径的程序名,在
PATH
环境变量的目录列表中搜索这个程序。

带有字母l(表示list)的
exec
函数要求将新程序的每个命令行参数都当作一个参数传给它,命令行参数的个数是可变的,因此函数原型中有
...
...
中的最后一个可变参数应该是
NULL
,起sentinel的作用。对于带有字母v(表示vector)的函数,则应该先构造一个指向各参数的指针数组,然后将该数组的首地址当作参数传给它,数组中的最后一个指针也应该是
NULL
,就像
main
函数的
argv
参数或者环境变量表一样。

对于以e(表示environment)结尾的
exec
函数,可以把一份新的环境变量表传给它,其他
exec
函数仍使用当前的环境变量表执行新程序。

exec
调用举例如下:
char*constps_argv[]={"ps","-o","pid,ppid,pgrp,session,tpgid,comm",NULL};
char*constps_envp[]={"PATH=/bin:/usr/bin","TERM=console",NULL};
execl("/bin/ps","ps","-o","pid,ppid,pgrp,session,tpgid,comm",NULL);
execv("/bin/ps",ps_argv);
execle("/bin/ps","ps","-o","pid,ppid,pgrp,session,tpgid,comm",NULL,ps_envp);
execve("/bin/ps",ps_argv,ps_envp);
execlp("ps","ps","-o","pid,ppid,pgrp,session,tpgid,comm",NULL);
execvp("ps",ps_argv);


(2)vfork-exec

vfork比起fork函数更快,二者的区别如下:

a)vfork创建的子进程并不复制父进程的数据,在随后的exec调用中系统会复制新程序的数据到内存,继而避免了一次数据复制过程
b)父进程以vfork方式创建子进程后将被阻塞,知道子进程退出或执行exec调用后才能继续运行.当子进程只用来执行新程序时,vfork-exec模型比fork-exec模型具有更高的效率,这种方法也是Shell创建新进程的方式.

#include<sys/types.h>

#include<unistd.h>

#include<stdio.h>

intmain()

{

pid_tpid;

if((pid=vfork())==0){

fprintf(stderr,"----begin----\n");

sleep(3);

execl("/bin/uname","uname","-a",0);

fprintf(stderr,"----end----\n");

}

elseif(pid>0)

fprintf(stderr,"forkchildpid=[%d]\n",pid);

else

fprintf(stderr,"Forkfailed.\n");

return0;

}

[bill@billstoneUnix_study]$makeexec2

make:`exec2'isuptodate.

[bill@billstoneUnix_study]$./exec2

----begin----

forkchildpid=[13293]

[bill@billstoneUnix_study]$Linuxbillstone2.4.20-8#1ThuMar1317:18:24EST2003i686athloni386GNU/Linux


(3)system

在UNIX中,我们也可以使用system函数完成新程序的执行.

函数system会阻塞调用它的进程,并执行字符串string中的shell命令.

[bill@billstoneUnix_study]$catexec3.c

#include<unistd.h>

#include<stdio.h>

intmain()

{

charcmd[]={"/bin/uname-a"};

system(cmd);

return0;

}

[bill@billstoneUnix_study]$makeexec3

ccexec3.c-oexec3

[bill@billstoneUnix_study]$./exec3

Linuxbillstone2.4.20-8#1ThuMar1317:18:24EST2003i686athloni386GNU/Linux


进程休眠:sleep


进程终止:exitabort


进程同步(等待):wait

一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用
wait
waitpid
获取这些信息,然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在Shell中用特殊变量
$?
查看,因为Shell是它的父进程,当它终止时Shell调用
wait
waitpid
得到它的退出状态同时彻底清除掉这个进程。

如果一个进程已经终止,但是它的父进程尚未调用
wait
waitpid
对它进行清理,这时的进程状态称为僵尸(Zombie)进程。

ps-ef|grep13707

bill137071441004:17pts/000:00:00./szomb1

bill1370813707004:17pts/000:00:00[szomb1<defunct>]//僵死进程

bill137101441004:17pts/000:00:00grep13707

[bill@billstoneUnix_study]$

其中,'defunct'代表僵死进程.对于僵死进程,不能奢望通过kill命令杀死之,因为它已经'死'了,不再接收任何系统信号.

当子进程终止时,它释放资源,并且发送SIGCHLD信号通知父进程.父进程接收SIGCHLD信号,调用wait返回子进程的状态,并且释放系统进程表资源.故如果子进程先于父进程终止,而父进程没有调用wait接收子进程信息,则子进程将转化为僵死进程,直到其父进程结束.

一旦知道了僵死进程的成因,我们可以采用如下方法预防僵死进程:

(1)wait法

父进程主动调用wait接收子进程的死亡报告,释放子进程占用的系统进程表资源.

(2)托管法

如果父进程先于子进程而死亡,则它的所有子进程转由进程init领养,即它所有子进程的父进程ID号变为1.当子进程结束时init为其释放进程表资源.

托管法技巧:两次fork,子进程退出,则子子进程的父进程变为init。

(3)忽略SIGC(H)LD信号

当父进程忽略SIGC(H)LD信号后,即使不执行wait,子进程结束时也不会产生僵死进程.

(4)捕获SIGC(H)LD信号

当父进程捕获SIGC(H)LD信号,并在捕获函数代码中等待(wait)子进程

wait和waitpid函数的原型是:


#include<sys/types.h>

#include<sys/wait.h>


pid_twait(int*status);

pid_twaitpid(pid_tpid,int*status,intoptions);

若调用成功则返回清理掉的子进程id,若调用出错则返回-1。父进程调用wait或waitpid时可能会:


阻塞(如果它的所有子进程都还在运行)。


带子进程的终止信息立即返回(如果一个子进程已终止,正等待父进程读取其终止信息)。


出错立即返回(如果它没有任何子进程)。


这两个函数的区别是:


如果父进程的所有子进程都还在运行,调用wait将使父进程阻塞,而调用waitpid时如果在options参数中指定WNOHANG可以使父进程不阻塞而立即返回0。


wait等待第一个终止的子进程,而waitpid可以通过pid参数指定等待哪一个子进程。


可见,调用wait和waitpid不仅可以获得子进程的终止信息,还可以使父进程阻塞等待子进程终止,起到进程间同步的作用。如果参数status不是空指针,则子进程的终止信息通过这个参数传出,如果只是为了同步而不关心子进程的终止信息,可以将status参数指定为NULL。


例30.6.waitpid


#include<sys/types.h>

#include<sys/wait.h>

#include<unistd.h>

#include<stdio.h>

#include<stdlib.h>


intmain(void)

{

pid_tpid;

pid=fork();

if(pid<0){

perror("forkfailed");

exit(1);

}

if(pid==0){

inti;

for(i=3;i>0;i--){

printf("Thisisthechild\n");

sleep(1);

}

exit(3);

}else{

intstat_val;

waitpid(pid,&stat_val,0);

if(WIFEXITED(stat_val))

printf("Childexitedwithcode%d\n",WEXITSTATUS(stat_val));

elseif(WIFSIGNALED(stat_val))

printf("Childterminatedabnormally,signal%d\n",WTERMSIG(stat_val));

}

return0;

}


子进程的终止信息在一个int中包含了多个字段,用宏定义可以取出其中的每个字段:如果子进程是正常终止的,WIFEXITED取出的字段值非零,WEXITSTATUS取出的字段值就是子进程的退出状态;如果子进程是收到信号而异常终止的,WIFSIGNALED取出的字段值非零,WTERMSIG取出的字段值就是信号的编号。作为练习,请读者从头文件里查一下这些宏做了什么运算,是如何取出字段值的。



守护进程

所谓守护进程是一个在后台长期运行的进程,它们独立于控制终端,周期性地执行某项任务,或者阻塞直到事件发生,默默地守护着计算机系

统的正常运行.在UNIX应用中,大部分socket通信服务程序都是以守护进程方式执行.

完成一个守护进程的编写至少包括以下几项:

(1)后台执行

后台运行的最大特点是不再接收终端输入,托管法可以实现这一点

pid_tpid;

pid=fork();

if(pid>0)exit(0);//父进程退出

/*子进程继续运行*/

父进程结束,shell重新接管终端控制权,子进程移交init托管

(2)独立于控制终端

在后台进程的基础上,脱离原来shell的进程组和session组,自立门户为新进程组的会话组长进程,与原终端脱离关系

#include<unistd.h>

pid_tsetsid();

函数setsid创建一个新的session和进程组.

(3)清除文件创建掩码

进程清除文件创建掩码,代码如下:

umask(0);

(4)处理信号

为了预防父进程不等待子进程结束而导致子进程僵死,必须忽略或者处理SIGCHLD信号,其中忽略该信号的方法为:

signal(SIGCHLD,SIG_IGN);

守护进程独立于控制终端,它们一般以文件日志的方式进行信息输出.Syslog是Linux中的系统日志管理服务,通过守护进程syslogd来维护。该守护进程在启动时会读一个配置文件“/etc/syslog.conf”。该文件决定了不同种类的消息会发送向何处。例如,紧急消息可被送向系统管理员并在控制台上显示,而警告消息则可记录到一个文件中。该机制提供了3个syslog函数,分别为openlog、syslog和closelog。

下面是一个简单的守护进程实例InitServer

[bill@billstoneUnix_study]$catinitServer.c

1:#include<assert.h>

2:

3:#include<signal.h>

4:

5:#include<sys/wait.h>

6:

7:#include<sys/types.h>

8:

9:voidClearChild(intnSignal){

10:

11:pid_tpid;

12:

13:intnState;

14:

15://WNOHANG非阻塞调用waitpid,防止子进程成为僵死进程

16:

17:while((pid=waitpid(-1,&nState,WNOHANG))>0);

18:

19:signal(SIGCLD,ClearChild);//重新绑定SIGCLD信号

20:

21:}

22:

23:intInitServer(){

24:

25:pid_tpid;

26:

27:assert((pid=fork())>=0);//创建子进程

28:

29:if(pid!=0){//父进程退出,子进程被init托管

30:

31:sleep(1);

32:

33:exit(0);

34:

35:}

36:

37:assert(setsid()>=0);//子进程脱离终端

38:

39:umask(0);//清除文件创建掩码

40:

41:signal(SIGINT,SIG_IGN);//忽略SIGINT信号

42:

43:signal(SIGCLD,ClearChild);//处理SIGCLD信号,预防子进程僵死

44:

45:return0;

46:

47:}

48:

49:intmain()

50:

51:{

52:

53:InitServer();

54:

55:sleep(100);

56:

57:return0;

58:

59:}


[bill@billstoneUnix_study]$makeinitServer

ccinitServer.c-oinitServer

[bill@billstoneUnix_study]$./initServer

[bill@billstoneUnix_study]$ps-ef|grepinitServer

bill137211004:40?00:00:00./initServer//'?'代表initServer独立于终端

bill137251441004:41pts/000:00:00grepinitServer

程序在接收到SIGCLD信号后立即执行函数ClearChild,并调用非阻塞的waitpid函数结束子进程结束

信息,如果结束到子进程结束信息则释放该子进程占用的进程表资源,否则函数立刻返回.这样既保证了不增加守护进程负担,又成功地预防了僵死进程的产生.

自己编写的一个程序:

#cattest.c

1:#include<unistd.h>

2:#include<stdio.h>

3:#include<sys/types.h>

4:

5:intcal()

6:{

7:

8:inti=0,sum=0;

9:

10:for(i=0;i<=100;i++)

11:

12:{

13:

14:sum+=i;

15:

16:}

17:

18:returnsum;

19:

20:}

21:

22:int

23:

24:main()

25:

26:{

27:

28:intnum=1,status;

29:

30:int*s=#

31:

32:pid_tpid;

33:

34:if((pid=fork())==0)

35:

36:{

37:

38:*s=cal();

39:

40:printf("1+..+100=%d\n",*s);

41:

42:exit(0);

43:

44:}

45:

46:elseif(pid<0)

47:

48:{

49:

50:exit(0);

51:

52:}

53:

54://pid=wait(&status);

55:

56://if(status==0)

57:

58://{

59:

60:wait();

61:

62:printf("1+2+...+100=%d\n",*s);

63:

64://}

65:

66://else

67:

68://{

69:

70://printf("error!\n");

71:

72://}

73:

74:}

75:

76:[root@localhostchapter9]#./test

77:

78:1+..+100=5050

79:

80:1+2+...+100=1

81:


程序的本意是用子进程来执行函数,而fork子进程完全复制父进程数据空间,这样子进程会获得父进程的所有变量的拷贝,尽管父子进程变量名相同,但却存在了不同的地方,因此不能通过内存变量完成父子进程之间的信息传递。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: