第8章 进程控制
*进程标识
每个进程由一个正整数唯一标识,若该进程终止,它的进程标识整数可以被其他进程使用。
L/Unix系统中 0进程表示swapper进程,它不执行磁盘上的代码,所以被称为系统进程。
进程1,也就是init进程负责在自举后启动一个UNIX系统,通常配合/etc/rc*等文件启动系统。
*关于fork
.fork是创建一个新进程的唯一方式,它调用一次,返回两次。然后两者都运行接下来的代码。
.子进程获得父进程的数据空间,堆栈,的副本。所以操作子进程的变量不会影响父进程。
.子进程返回0
.父进程返回子进程pid
.但是是父进程还是子进程先运行是不确定的,用vfork可以保证子进程先运行。
*进程模型有哪些?
.进程扇
进程扇,的各个子进程互为兄弟关系。
.进程链
该模型把生成的子进程作为父进程再来生成子进程,如此下去。
.网络常用模型
服务器的常见模型是,为每个客户端生成一个子进程,而父进程继续监听其他的连接。
*退出状态和终止状态的区别?
.退出状态:进程正常退出的返回值,如调用exit(1)或_exit(1)。
.终止状态:进程异常终止的状态,由父进程调用wait和waitpid获取。
*如何获取一个子进程的终止状态?
.非阻塞方式
(1)signal处理方式
static void sigchld_handler(int sig) { /* SIGCHLD detected */
int save_errno;
int status;
save_errno=errno;
while(waitpid(-1, &status, WNOHANG)>0) {
--num_clients; /* one client less */
}
...
signal(SIGCHLD, sigchld_handler); //必须再调用一次
errno=save_errno;
}
(2)调用waitpid方式
waitpid(-1, NULL, WNOHONG);
.阻塞方式
获取任意一个子进程的终止状态:pid_t wait(&stat); waitpid(-1, NULL, 0)
获取特定子进程的终止状态:waitpid(pid, NULL, 0);
.打印进程的终止状态
do {
w = waitpid(cpid, &status, WUNTRACED | WCONTINUED);
if (w == -1) { perror("waitpid"); exit(EXIT_FAILURE); }
if (WIFEXITED(status)) { //正常终止
printf("exited, status=%d/n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) { //异常终止;常用这个
printf("killed by signal %d/n", WTERMSIG(status));
} else if (WIFSTOPPED(status)) { //暂停子进程
printf("stopped by signal %d/n", WSTOPSIG(status));
} else if (WIFCONTINUED(status)) { //从暂停中恢复的子进程
printf("continued/n");
}
} while (!WIFEXITED(status) && !WIFSIGNALED(status));
exit(EXIT_SUCCESS);
}
*获取任意一个子进程的状态?
void child_status(void) {
int pid, status;
while((pid=waitpid(-1, &status, WNOHANG))>0) {
s_log(LOG_INFO, "Child process %d finished with status %d",
pid, status);
}
*僵死进程如何产生?
.一个进程终止时,内核会自动检测其子进程,并调用init(进程号为1)进程收养。
.如果一个子进程终止,而父进程没有调用wait或waitpid就会产生僵死进程。
例程:
int main(void) { pid_t pid; if ((pid = fork()) < 0) { perror("fork():"); exit(1); } else if (pid == 0) { //child exit(0); } else { //parent system("ps"); } }
这段代码可能会产生僵死进程。
儿子进程产生后,就立即退出,父进并没有调用wait和waitpid来获取子进程的状态,所以产生僵死进程。
但是,如果在子进程退出的时候父进程已经退出,那么子进程将会由init进程收养,这时将不会产生僵死进程。
*如何避免僵死进程?
根据僵死进程产生的方式,可以有以下两种方式避免僵死进程:
.调用wait或waitpid
.调用fork两次
例如下面的代码:
/* * 测试如何避免僵死进程。 * * 法1: 使用wait或waitpid * 法2: fork两次 * */ #include "common.h" void pr_exit(int status); //#define USEWAIT 1 #ifdef USEWAIT //使用wait/waitpid方式避免僵死进程 int main(void) { pid_t pid; int st; if ((pid = fork()) < 0) { perror("fork()"); exit(1); } else if (pid == 0) { //child printf("I am child : %d, parent is :%d/n", getpid(), getppid()); abort(); } else { //parent waitpid(-1, &st, 0); //获取子进程的状态,避免了僵死进程 pr_exit(st); system("ps"); } exit(1); } #else //使用fork两次的方式避免僵死进程 int main(void) { pid_t pid; int st; if ((pid = fork()) < 0) { perror("fork()"); exit(1); } else if (pid == 0) { //child printf("I am child : %d, parent is :%d/n", getpid(), getppid()); if ((pid = fork()) < 0) { //第2次调用fork perror("fork() 2:"); } else if (pid > 0) //parent退出,第2个子进程由init收养不会出现僵死进程 exit(0); //第2个子进程在这里运行代码 printf("I am second child : %d, parent is :%d/n", getpid(), getppid()); sleep(2); exit(0); } else { //parent waitpid(-1, &st, 0); pr_exit(st); system("ps"); } exit(0); } #endif void pr_exit(int status) { if (WIFEXITED(status)) { //正常终止 printf("exited, status=%d/n", WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { //异常终止;常用这个 printf("killed by signal %d/n", WTERMSIG(status)); } else if (WIFSTOPPED(status)) { //暂停子进程 printf("stopped by signal %d/n", WSTOPSIG(status)); } else if (WIFCONTINUED(status)) { //从暂停中恢复的子进程 printf("continued/n"); } }
.调用信号处理函数signal处理信号SIGCHLD
void sigchld_handler(int signo) { int status; pid_t pid; if((pid = waitpid(-1, &status, WNOHANG)) > 0) printf("I am %d, now exit .../n", pid); signal(SIGCHLD, sigchld_handler); //必须再调用一次 }
调用该信号处理函数可以处理多个子进程的情况。
但何时使用信号处理函数,何时直接在程序中使用waitpid需要更具实际情况具体考虑。 ????
*exec 函数
.子进程往往调用一种exec函数执行另一个程序。 exec函数将该进程替换为全新的程序。包括正文段,数据,堆和栈段。
但由于是在生成一个进程后运行的,所以其进程ID不会变化。
第9章 进程关系
*基本概念
.进程组:一个或多个进程的集合,每个进程组有一个唯一的进程组ID,由函数getpgrp返回调用进程的进程组组ID。
#include <unistd.h>
//返回进程pid的进程组ID,若pid为0则返回调用进程的进程组ID
pid_t getpgid(pid_t pid);
//返回调用进程的进程组ID
pid_t getpgrp(void);
.进程组长:每个进程都有一个进程组长,组长进程的标识是,其进程组ID等于其进程ID。
.组长进程可以创建一个新的进程组,创建该组中的进程,然后终止。
.进程组不依赖于组长的存在,只要进程组中有一个进程存活,该进程组就存活。
.一个进程组中的最后一个进程,可以终止,或也可以转义到另一个进程组。
.一个进程只能为自己或它的子进程设置进程组ID,但在调用exec函数后,进程组ID就不能更改。
*会话(session)
.会话是一个或多个进程组的集合。
例: $proc1 | proc2 &
proc3 | proc4 | proc5
.创建一个新会话?
#include <unistd.h>
pid_t setsid(void)
若调用该进程是一个进程组的组长,则出错。
若调用该进程的不是进程组组长,那么:
(1)该进程变成新会话首进程。此时该进程是该会话期唯一进程。
(2)该进程称为一个进程组的组长进程,进程组ID是该调用进程的ID。
(3)该进程没有控制终端。原来若有,则这些联系将中断。
*如何保证正确创建一个会话(session)?
...
pid_t pid;
if ((pid = fork()) < 0)
exit(1);
else if (pid > 0) //parent
exit(1);
else { //child
...
setsid();
...
}
wait(&stat);
...
说明:setsid的定义可以直到,如果调用该函数的进程是一个进程组长,那么则出错,为了使得调用setsid的函数不是会话的首进程,我们可以先让父进程退出,让子进程执行该函数。由于子进程继承了父进程的进程组ID,而它的进程ID是新生成的,两者不可能相等,所以保证了setsid的执行的正确性。
*如何获取会话首进程?
.会话首进程是指:开始创建会话时的唯一的单个进程。
pid_t getsid(void) //用来获取会话首进程的进程ID号
*使用如下函数能获得哪一个进程组是前台进程组:
#include <sys/types.h>
#include <unistd.h>
pid_t tcgetpgrp(int filedes);
返回:若成功则为前台进程组ID,若出错则为-1
int tcsetpgrp(int filedes, pid_t pgrpid);
返回:若成功则为0 ,若出错则为-1
函数tcgetpgrp返回前台进程组ID,它与在filedes上打开的终端相关。
如果进程有一个控制终端,则该进程可以调用tcsetpgrp将前台进程组ID设置为pgrpid。pgrpid值应当是在同一对话期中的一个进程组的ID。filedes必须引用该对话期的控制终端。
大多数应用程序并不直接调用这两个函数。它们通常由作业控制shell调用。只有定义了_POSIX_JOB_CONTROL,这两个函数才被定义了,否则它们返回出错。
#include <unistd.h>
pid_t tcgetsid(int filedes);
得到会话首进程的会话ID。
*控制终端
.用户登陆时,会自动建立控制终端。
.一个会话可以有一个控制终端,也可以没有控制终端。
.建立与控制终端连接的会话首进程被称为控制进程。
.如果一个会话有一个控制终端,它只有一个前台进程组,其他为后台进程组。
.无论何时键入ctrl+c或ctrl+/或终端已经断开,这些事件的信号将会发送给前台进程组的所有进程。
*作业控制
作业就是一个进程组,一个终端上允许启动多个作业。作业控制就是控制哪些作业可以访问终端,哪些作业在后台运行。
.只有前台作业接受终端的输入,若后台作业也想试图读终端,终端驱动程序向后台作业发送信号SIGTTIN。该信号会停止后台作业,等待用用户把后台作业转移到前台,然后此作业开始读终端。例如:
#在后台运行cat,它试图从终端读
[zh@linux test]$ cat > temp.foo &
[1] 10398
#终端驱动程序直到它是一个后台进程,于是发送SIGTTIN
#可以看到该后台作业已经被停止
[zh@linux test]$ jobs
[1]+ Stopped cat >temp.foo #
[zh@linux test]$
#用fg命令把该停止的作业送入前台(SIGCONT),现在它继续运行
[zh@linux test]$ fg 1
cat >temp.foo
adf
adf
试图把读终端的后台作业在后台启动,被终端驱动程序自动终止。
[zh@linux test]$ cat >temp.foo&
[1] 10403
[zh@linux test]$ bg 1 #企图把该作业在后台运行
[1]+ cat >temp.foo &
[1]+ Stopped cat >temp.foo #再次被驱动程序停止
[zh@linux test]$ jobs
[1]+ Stopped cat >temp.foo
.若后台作业企图输出到控制终端,这是一个我们可以控制的选项
#试图在后台向终端输出文件内容
[zhengxh@jsdlinux test]$ cat temp.foo &
[1] 10460
[zhengxh@jsdlinux test]$ 1
2
3
[1]+ Done cat temp.foo
[zhengxh@jsdlinux test]$ jobs
#用tostop禁止从后台向终端输出
[zhengxh@jsdlinux test]$ stty tostop
#这次没有输出而是发送了信号,SIGTOUT
[zhengxh@jsdlinux test]$ cat temp.foo &
[1] 10462
#可以看到进程被停止
[zhengxh@jsdlinux test]$ jobs
[1]+ Stopped cat temp.foo
#把它转移到前台,它又继续运行了
[zhengxh@jsdlinux test]$ fg 1
cat temp.foo
1
2
3
*孤儿进程组
一个其父进程已经终止的进程被称为孤儿进程,这种进程由init进程收养。
孤儿进程组的定义:
.该进程组中每个成员的父进程要么是该组的一个成员,要么不是该组所属会话的成员。
.一个进程组不是孤儿进程组的条件是,该进程组中有一个进程,其父进程在属于同一会话的另一个组中。
如果进程组不是孤儿进程组,那么在属于同一会话的另一个组中的父进程就有机会重启该组中停止的进程。
POSIX.1要求向新孤儿进程组中处于停止状态的每一个进程发送挂断信号(SIGHUP),接着又向其发送继续信号(SIGCONT)。在处理了挂断信号后,子进程继续。对挂断信号的系统默认动作是终止该进程,为此必须提供一个信号处理程序以捕捉该信号。
//创建孤儿进程组的例子
int
main(void)
{
char c;
pid_t pid;
if ((pid = fork()) < 0)
exit(0);
else if (pid > 0) { //parent
sleep(5); //得让子进程先安装后信号处理函数,并停止它
exit(0);
} else {
//POSIX的要求向向新的孤儿进程组中处于停止状态的进程发送SIGHUP和SIGCONT信号
signal(SIGHUP, sig_hup); //捕捉父进程传过来的SIGHUP信号
kill(getpid(), SIGSTP); //停止自己,相当于ctrl+z
if(read(STDIN_FILENO, &c, 1) != 1)
printf("read from stdin out");
exit(0);
}
}