您的位置:首页 > 其它

第九章 进程关系

2015-01-19 19:38 176 查看
第九章、 进程关系

如果用户正确登录, login程序就完成以下工作:

将当前工作目录更改为用户的起始目录(chdir)

调用 chown 更改获得终端的所有权,使登录用户成为它的所有者

将对该终端的设备的访问权限改变成 “用户读和写”

调用 setgid 和 initgroups 设置进程的组ID

用login获得的所有信息初始化环境(设置环境变量的值):

初始目录(HOME)、shell(SHELL)、用户名(USER和LOGNAME)、以及系统默认路径(PATH)

login 进程更改为登录用户的用户ID(setuid),并调用登录shell(在/etc/passwd中)

execl("/bin/sh","-sh",(char *0) 0); 【sh前的 - 表示这里的shell被作为登录shell调用】

网络链接

-------------------暂时跳过

进程组

每个进程除了有个进程ID之外,还属于一个进程组。每个进程组有一个唯一的进程组ID。可通过函数 getpgrp 返回进程组ID。

进程组是一个或多个进程的集合,通常是在同一作业中结合起来的。

同一进程组中的各进程接收来自同一终端的各种信号。

每个进程组都有一个组长进程,组长进程的进程组ID 等于 其进程ID。

进程组组长可以创建一个进程组、创建该组中的进程,然后终止。只要在某个进程组中有一个进程存在,则该进程组就存在,与组长进程是否还存在无关


进程组的生命期即为从进程组创建开始到其中最后一个进程离开为止。

函数 setpgid(pid, pgid)

可以加入一个现有的进程组或者创建一个新进程组

该函数将 pid 进程的进程组进程组ID设置为pgid。 如果这两个参数相等,则由pid制定的进程变成进程组组长。

如果 pid 是 0, 则使用调用者的进程ID。 如果pgid是 0 ,则由pid指定的进程ID用作进程组ID

一个进程只能为它自己或它的子进程设置进程组ID,在它的子进程调用了exec后,它就不再更改该子进程的进程组ID。

在大多数作业控制shell中,在fork之后调用此函数,使父进程设置其子进程的进程组ID,并且也使子进程设置其自己的进程组ID。

虽然这两个调用中有一个是冗余的,但让父进程和子进程全都这样做可以保证,在父进程和子进程认为子进程已进入了该进程组之前,这确实已经发生了。

若不这样做,在fork之后,由于父进程和子进程运行的先后次序不确定,会因为子进程的组员身份取决于哪个进程首先执行而产生竞争条件。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
pid_t pid;

if((pid=fork()) < 0)
{
perror("fork");
exit(1);
}
else if(0 == pid)
{
printf("The child PID is %d\n",getpid());
printf("THe Group ID of child is %d\n",getpgid(0));
sleep(5);
printf("THe Group ID of child is %d\n",getpgid(0));
exit(0);
}

sleep(1);
setpgid(pid,pid);
printf("The parent has changed the gid of his child\n");

sleep(5);
printf("THe parent PID is %d\n",getpid());
printf("THe Group ID of parent is %d\n",getpgid(0));

return 0;
}


$
The child PID is 5200
THe Group ID of child is 5199
The parent has changed the gid of his child
THe Group ID of child is 5200
THe parent PID is 5199
THe Group ID of parent is 5199


以后讨论信号时候,会提及如何将一个信号发给一个进程或一个进程组。

waitpid也可以等待一个进程或者指定进程组中的一个进程终止。

会话

会话(session)是一个或多个进程组的集合,通常是由shell的管道将几个进程编成一组的。

函数 setsid

建立一个新的会话

如果调用此函数的进程不是一个进程组的组长,则此函数会创建一个新的会话:

一、该进程变成新会话的会话首进程(session leader,即创建该会话的进程),此时该进程是会话中的唯一进程

二、该进程成为一个新进程组的组长进程,新进程组ID是该调用进程的进程ID

三、该进程没有控制终端。即使在调用setsid之前该进程有个控制终端,那么这种联系也会被切断(没懂。。。下节会讨论)

如果该进程已经是一个进程组组长,则此函数返回出错(为避免:通常是先调用fork,然后使父进程终止,而子进程继续,这就保证了子进程不是一个进程组组长)

函数 getsid(pid)

若pid为0,即返回调用进程的会话首进程的进程组ID。

若pid并不属于调用者所在的会话,那么就不能得到该会话首进程的进程组ID。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
pid_t pid;

if ((pid=fork())<0) {
printf("fork error!");
exit(1);
}else if (pid==0) {
printf("The child process PID is %d.\n",getpid());
printf("The Group ID of child is %d.\n",getpgid(0));
printf("The Session ID of child is %d.\n",getsid(0));
sleep(10);
setsid(); // 子进程非组长进程,故其成为新会话首进程,且成为组长进程。该进程组id即为会话进程
printf("Changed:\n");
printf("The child process PID is %d.\n",getpid());
printf("The Group ID of child is %d.\n",getpgid(0));
printf("The Session ID of child is %d.\n",getsid(0));
sleep(20);
exit(0);
}

return 0;
}


$ The child process PID is 3000.
THe Group ID of child is 2999.
The Session ID of child is 1999.
Changed:
The child process PID is 3000.
THe Group ID of child is 3000.
The Session iD of child is 3000.


网上有一段:

(http://zhidao.baidu.com/link?url=KKkk5PDgexTbFvYfsFhkL-f5YW1BBCCS8Z2I6893CTtbby6txYM78Eo--1nFC2WszDupWaVJR7jS_SeyT-Idsa)

有一个方法让任何进程都不成为组长进程,就是创建一个新进程,终止父进程,让子进程调用setsid(),下一步还应该调用一次fork(),

父进程退出,子进程关闭继承于父进程打开的文件,修改自己的工作目录,然后正式成为一个daemon进程。

实验了一下发现,一个会话首进程如果被终止,那么它里面的前台进程组会被发送SIGHUP信号(默认处理是终止进程),后台进程组们则不会受到影响

比如打开一个终端,前台进程正在运行,也有自建的一个后台进程在运行。通过另一个终端将此终端kill掉后,发现只剩下了一个后台进程在运行了,

且已经被init领养了。

控制终端:

1、一个会话可以有一个控制终端(controlling terminal)。通常使终端设备(在终端登录情况下)或伪终端设备(在网络登录情况下)

2、建立与控制终端连接的会话首进程被称为控制进程(controlling process)

3、一个会话中的几个进程组可被分为一个前台进程组(froeground process group)以及一个或多个后台进程组(background process group)

4、如果一个会话有一个控制终端,则它有一个前台进程组,其他组为后台进程组。

5、无论何时键入终端的终端键(ctrl+C),都会将中断信号发送至前台进程组的所有进程

6、无论何时键入终端的终端键(ctrl+\),都会将中断信号发送至前台进程组的所有进程

7、如果终端接口检测到调制解调器断开,则将挂断信号发送至控制进程(会话首进程)


函数 tcgetpgrp

返回前台进程组ID。

函数 tcsetpgrp

可用来设置前台进程组ID。

函数 tcgetsid

给出控制TTY的文件描述父,就能获得会话首进程的进程ID

作业控制

首先,我们总结一下之前所学的一些基本概念:(以下基本概念是从 http://blog.csdn.net/u012243115/article/details/42105089 博客悄悄拿来的 ... 别举报我)

僵死进程:一个子进程已经终止,但是其父进程没有对其进行善后处理(获取终止子进程有关信息,释放它仍占有的资源),

则该子进程就成为僵死进程。消灭僵尸进程的唯一方法是终止其父进程。

孤儿进程:子进程的父进程已经终止,但是该进程依然存在,则称该子进程为孤儿进程。孤儿进程会被init进程的收养。一个孤儿进程可以组成孤儿进程组。

会话首进程:新建会话时,会话中的唯一进程,其进程ID等于会话ID。它通常是一个登陆shell,也可以在成为孤儿进程后调用setsid()成为一个新会话。

会话:一个或多个进程组的集合。一个登陆shell发起的会话,一般由一个会话首进程、一个前台进程组、一个后台进程组组成。

进程组:一个或多个进程的集合,进程组属于一个会话。fork()并不改变进程组ID。

进程组组长:进程ID与其所在进程组ID相等的进程。组长可以改变子进程的进程组ID,使其转移到另一进程组。例如一个shell进程(下文均以bash为例),

当使用管道线时,如echo "hello" | cat,bash以第一个命令的进程ID为该管道线内所有进程设置进程组ID。此时echo和cat的进程组ID都设置成echo的进程ID。

前台进程组:该进程组中的进程能够向终端设备进行读、写操作的进程组。登陆shell(例如bash)通过调用tcsetpgrp()函数设置前台进程组,

该函数将终端设备的fd(文件描述符)与指定进程组关联。成为前台进程组的进程其控制终端进程组ID等于进程组ID,常常可以通过比较他们来判断前后台进程组。

后台进程组:一个会话中,除前台进程组、会话首进程以外的所有进程组。该进程组中的进程能够向终端设备写,但是当试图读终端设备时,

将会收到SIGTTIN信号,并停止。登录shell可以根据设置在终端上发出一条消息通知用户有进程欲求读终端。

再来继续作业控制的概念:

允许在一个终端上启动多个作业(进程组),控制哪个作业可以访问终端以及哪些作业在后台运行

如果后台程序想要从终端读取数据,则会收到信号 SIGTTIN,停止该后台作业 比如 cat > /dev/null

如果后台程序想要输出到终端,那么可以通过 stty 设置是否允许输出(默认允许,可以通过命令 stty tostop禁止)

在我使用的 bash 下:

$ ps -o pid,ppid,pgid,sid,comm | cat

PID PPID PGID SID COMMAND

3244 3240 3244 3244 bash

5567 3244 5567 3244 ps

5568 3244 5567 3244 cat

$

$ ps -o pid,ppid,pgid,sid,comm | cat &

[1] 5580

$ PID PPID PGID SID COMMAND

3244 3240 3244 3244 bash

5579 3244 5579 3244 ps

5580 3244 5579 3244 cat

$ sleep 10 &

[1] 5629

$ ps -o pid,ppid,pgid,sid,comm | cat

PID PPID PGID SID COMMAND

3244 3240 3244 3244 bash

5629 3244 5629 3244 sleep

5630 3244 5630 3244 ps

5631 3244 5630 3244 cat

$

我们可以发现,这个会话SID几乎是不变的,因为这是一个登陆shell发起的会话,一般由一个会话首进程、一个前台进程组、一个(多个)后台进程组组成。

使用管道的命令,虽然也都是 shell fork之后处理的(因此父进程都是shell),但它们是一个进程组,组长由第一个执行的进程担当。

孤儿进程

一个它的父进程已经终止的进程称为孤儿进程,由init收养。

在对孤儿进程代码实验的时候,顺便对僵死进程实验了一下,发现一些发现一些问题,即在父进程运行期间,若子进程结束,父进程不采取wait措施,通过另一个终端用 ps 发现

确实出现了僵死进程。但父进程结束后,僵死进程就消失了,这就让我产生了疑惑。

在网上搜寻了一段时间后,发现一些能解决我疑惑但无法确信其是否正确的言论,这里先贴上来:

孤儿进程并不会有什么危害,真正会对系统构成威胁的是僵死进程。那么,什么情况下僵死进程会威胁系统的稳定呢?设想有这样一个父进程:

它定期的产生一个子进程,这个子进程需要做的事情很少,做完它该做的事情之后就退出了,因此这个子进程的生命周期很短,但是,

父进程只管生成新的子进程,至于子进程退出之后的事情,则一概不闻不问,这样,系统运行上一段时间之后,系统中就会存在很多的僵死进程,

倘若用ps命令查看的话,就会看到很多状态为Z的进程。严格地来说,僵死进程并不是问题的根源,罪魁祸首是产生出大量僵死进程的那个父进程。

因此,当我们寻求如何消灭系统中大量的僵死进程时,答案就是把产生大量僵死进程的那个元凶枪毙掉(也就是通过kill发送SIGTERM或者SIGKILL信号啦)。

枪毙了元凶进程之后,它产生的僵死进程就变成了孤儿进程,这些孤儿进程会被init进程接管,init进程会wait()这些孤儿进程,

释放它们占用的系统进程表中的资源,这样,这些已经“僵死”的孤儿进程就能瞑目而去了。


该段文字说僵死进程的父进程终止后,所有僵死进程都变为孤儿进程由init领养并用wait得以释放。

孤儿进程组

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>

static void sig_hup(int s)
{
printf("SIGHUP received, pid = %ld\n",getpid());
}

static void pr_ids(char *name)
{
printf("%s : pid = %ld, ppid = %ld, pgrp = %ld, tpgrp = %ld\n",name,(long)getpid(),(long)getppid(),(long)getpgrp(),(long)tcgetpgrp(1));
fflush(stdout);
}

int main()
{
char c;
pid_t pid;

pr_ids("parent");
if( ( pid=fork() ) < 0 )
{
exit(1);
}
else if(pid > 0)
{
sleep(5);
}
else
{
pr_ids("Child ");
signal(SIGHUP,sig_hup);    //子进程为SIGHUP建立了信号处理程序,这样就能观察SIGHUP是否已发送给了子进程
kill(getpid(),SIGTSTP);    //子进程用kill向其自身发送停止信号(类似于用终端挂起字符(ctrl+z)停止了一个前台作业)
pr_ids("child ");
if(read(0,&c,1) != 1)
{
perror("read---");
}
}
return 0;
}


$
parent : pid = 8272, ppid = 7816, pgrp = 8272, tpgrp = 8272
Child  : pid = 8273, ppid = 8272, pgrp = 8272, tpgrp = 8272
SIGHUP received, pid = 8273
child  : pid = 8273, ppid = 1, pgrp = 8272, tpgrp = 7816
read---: Input/output error


程序中的子进程,在其处于停止状态的时候,其父进程终止了,那么该子进程会如何呢? 子进程如何继续? 以及子进程是否知晓其已经是孤儿进程了呢?

在父进程终止后,子进程成为了一个孤儿进程组的成员。

孤儿进程组的定义:

该组中每个成员的父进程要么是该组的一个成员,要么不是改组所属会话的成员。

对孤儿进程组的另一个描述是:

一个进程组不是孤儿进程组的条件是 -----改组中有一个进程,其父进程在属于同一会话的另一组中。

如果该进程组不是孤儿进程组,那么属于同一会话的另一组中的父进程就有机会重新启动该组中停止的进程。

在这个程序中,进程组中的进程的父进程(即为init ,1)属于另一个会话,所以是孤儿进程组。

内核会向新的孤儿进程组中处于停止状态的每一个进程发送挂断信号(SIGHUP),接着又向其发送继续信号(SIGCONT)

那为什么进程在企图从标准输入读取信息的时候出错了呢?

1、 当父进程终止后,子进程变成后台进程组,因为父进程是由shell作为前台执行的。

2、之前提及,当后台进程企图从标准输入读取时,对后台进程组产生SIGTTIN。 但在这里,这是一个孤儿进程组,如果内核

用此信号停止它,则此进程就再也不会继续了。所以规定,read返回出错,errno设置为EIO。

最后,因为这一章属于概念理解,所以网上搜索的时候搜到一个题目挺符合的,搬运过来作为习题:

书上说kill掉父进程,其下的子进程也跟着消亡:

例如:

[root@qht2 ~]# ps -ef | grep httpd

root 3799 1 0 10:41 pts/0 00:00:00 /usr/sbin/nss_pcache off /etc/httpd/alias

root 3803 1 3 10:41 ? 00:00:00 /usr/sbin/httpd

apache 3807 3803 0 10:41 ? 00:00:00 /usr/sbin/httpd

apache 3808 3803 0 10:41 ? 00:00:00 /usr/sbin/httpd

apache 3809 3803 0 10:41 ? 00:00:00 /usr/sbin/httpd

apache 3810 3803 0 10:41 ? 00:00:00 /usr/sbin/httpd

apache 3811 3803 0 10:41 ? 00:00:00 /usr/sbin/httpd

apache 3812 3803 0 10:41 ? 00:00:00 /usr/sbin/httpd

apache 3813 3803 0 10:41 ? 00:00:00 /usr/sbin/httpd

apache 3814 3803 0 10:41 ? 00:00:00 /usr/sbin/httpd

root 3816 3749 0 10:42 pts/0 00:00:00 grep httpd

[root@qht2 ~]# kill 3803

[root@qht2 ~]# ps -ef | grep httpd

root 3820 3749 0 10:42 pts/0 00:00:00 grep httpd

显然kill掉父进程,子进程也消亡了!

这是正确的!

但为什么下面的情况,就不符合上面的说法!

我写了两个脚本a.sh 和b.sh, 在a.sh中调用b.sh,运行后显然有两个进程,而且a.sh为b.sh的父进程,然后我再另外一个terminal中kill 掉a.sh进程,但b.sh过寄给init进程,而不会终止!

我的例子如下:

[root@qht2 ~]# cat a.sh

#!/bin/sh

echo "A Begin"

./b.sh

echo "A End"

[root@qht2 ~]# cat b.sh

#!/bin/sh

echo "B Begin"

sleep 180

mkdir abcdef

echo "B End"

[root@qht2 ~]# ./a.sh

A Begin

B Begin

在这里等待(因为b.sh中有sleep 180)

打开另一个terminal,查看进程

[root@qht2 ~]# ps -ef | grep sh

。。。。。。

root 3984 3749 0 11:05 pts/0 00:00:00 /bin/sh ./a.sh

root 3985 3984 0 11:05 pts/0 00:00:00 /bin/sh ./b.sh ##显然b.sh是a.sh的子进程

root 3990 3838 0 11:05 pts/1 00:00:00 grep sh

[root@qht2 ~]# kill 3984

[root@qht2 ~]# ps -ef | grep sh

。。。。。。

root 3985 1 0 11:05 pts/0 00:00:00 /bin/sh ./b.sh

root 3992 3838 2 11:06 pts/1 00:00:00 grep sh

第一ternimal中的显示如下:

[root@qht2 ~]# ./a.sh

A Begin

B Begin

Terminated

但b.sh还是会运行(因为生成了abcdef目录)!

那位老大解释下两者的区别、原因

谢谢!

回答是:

父进程退出,子进程被init领养,继续运行,这才是正常的吧

而前一个,从名字看明显是一个守护进程,id=3803的是会话首进程,也是进程组的组长,KILL掉它,会导致SIGHUP发送给该进程组的每一个进程(就是所有父进程为3803的那些),默认情况下,SIGHUP会终止进程,所以全没了.

首先要清楚会话,进程组,控制终端这几个概念.

所有进程都是属于一个进程组的,而进程组又属于一个会话.

普通的进程所属的会话有控制终端,守护进程所属会话没有控制终端.

普通会话的首进程,同时也是建立与控制终端联系的进程,在它被KILL掉时,会向前台进程组发送SIGHUP信号.默认情况下,接收到SIGHUP的进程会被终止.此时后台进程组不受影响.

守护进程的会话,因为没有控制终端,所以就没有前后台进程组之分,会话首进程同时也是进程组组长.它被KILL掉会向该组每个进程发送SIGHUP,导致组中进程被中止.

楼主的第二个试验,一个脚本掉另一个脚本的行为,创建了一个新的进程组,脚本A是进程组组长,但却不是所在会话的首进程或控制进程,所以它被KILL掉,不影响同组的进程(脚本B),此时init进程会自动领养脚本B所在进程,并在它运行到结束时回收它所占用的资源.

楼主可以用ps -eo pid,ppid,pgrp,session,comm跑一下.

对于第一种情况,就是守护进程,应该会发现那一堆进程的session(会话ID)和pgrp(组ID)都一样且是相同的,而且正好等于子进程的ppid,同时也是你KILL掉那个进程的pid.这样可以证实你KILL掉的是会话首进程.


对于第二种情况,你会发现,进程A和B,session和pgrp是一样的,但两者却并不相同,session的值虽无法确定,但pgrp却应该正好是进程A的pid,这说明了进程A是组长但却不是会话首进程,所以KILL掉它不会导致子进程被结束.

但是,对于以上的答案,我并不是完全赞同,即这句话 : 【 守护进程的会话,因为没有控制终端,所以就没有前后台进程组之分,会话首进程同时也是进程组组长.它被KILL掉会向该组每个进程发送SIGHUP,导致组中进程被中止】 。通过以下程序,我在kill掉会话首进程(也是组长进程)后,发现sleep函数仍然在执行。。。且没有收到SIGHUP信号。

#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

void sig_hup(int s)
{
mkdir("/tmp/receive_sighup",0664);
}

int main()
{
pid_t pid;
char *str = "I am a test\n";
int fdcounts;
int i;
int fd;

if((pid=fork()) < 0)
{
perror("rk");
exit(1);
}
else if(pid > 0)                                        //exit the parent
exit(0);

setsid();                                               //create a new session to be independent
fdcounts = getdtablesize();
for(i=0; i<fdcounts; i++)
close(i);                                       //close all the opened descriptor
chdir("/");                                             //change working directory
umask(0);                                               //clear the umask

if((pid=fork()) < 0)

{
perror("fork");
exit(1);
}
else if(pid == 0)
{
signal(SIGHUP,sig_hup);
execlp("sleep","sleep","100",NULL);
exit(2);
}

for(;;)
{
fd = open("/tmp/deamontest",O_WRONLY | O_APPEND | O_CREAT, 0664);
if(fd < 0)
{
perror("open");
exit(1);
}

write(fd, str, strlen(str));
close(fd);
sleep(10);

printf("I still have a place for outputing ?\n");
}

return 0;
}

因此,这里我还是有些疑问存在。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: