首页 > 代码库 > 《unix环境高级编程》 读书笔记 (7)

《unix环境高级编程》 读书笔记 (7)

process control


1 process identifiers

每一个进程都有一个唯一的非负整型做为标识符。

#include <unistd.h>

pid_t getpid();
pid_t getppid();

pit_t getuid();
pit_t geteuid();

pit_t getgid();
pit_t getegid();

getpid: 返回进程ID
getppid: 返回父进程ID

getuid: 获得进程的real user ID
geteuid: 获得进程的effective user ID

关于real user ID和effective user ID:
Advanced Programming in the UNIX Environment (2) 第三节,链接:
http://blog.csdn.net/alex_my/article/details/39184461

getgid: 获取进程的real group ID
getegid: 获取进程的effective group ID

它们的用法在本文的后续章节中出现。


2 fork function

#include <unistd.h>

pid_t fork(void);

创建一个新的子进程,这个函数一次调用,两次返回,在原进程(即未来的父进程)中,返回的值为子进程ID,在子进程中,返回的值为0。子进程只能拥有一个副进程,可以通过getppid获取到父进程的ID。

理论上来说,子进程会复制一份父进程的数据空间,堆,栈。但是,往往在程序中,执行fork之后,常常在子进程中调用exec,该子进程完全由新的程序替换,覆盖了原先子进程复制的这部分数据空间,堆,栈,使得之前的复制白费力。因此,在大部分的实现中,子进程并不立马拷贝一份,而是使用一种"copy-on-write",当哪个进程要改变某一小部分内容的时候,就将小部分内存拷贝一份,供其使用。

至于子进程具体继承了什么,不继承什么,可以查询man 2 fork。

继承部分:
real user ID, effective user ID, real group ID, effective group ID
supplementary group IDs
process group ID
session ID
controlling terminal
ser-user-ID and set-group-ID flags
current working directory
root directory
file mode creation mask
signal mask and dispositions
the close-on-exec flag for any open file descriptors
environment
attached shared memory segments
memory mappings
resource limit


不继承部分:
child‘s tms_utime, tms_stime, tms_cutime, and tms_cstime are set to zero
file locks
pending alarms are cleared for the child
the set of pending signals for the child is set to the empty set


程序用例:

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

int main(int argc, char* argv[])
{
     int n = 1;

     printf("process id: %d \n", getpid());

     printf("test stdio buffer ");

     pid_t pid = fork();
     if(pid < 0)
     {
          printf("fork failed, error[ %d ] \t [ %s ]\n", errno, strerror(errno));
          exit(EXIT_FAILURE);
     }

     if(pid == 0)
     {
          printf("children, pid: %d \t ppid: %d \n", getpid(), getppid());
          ++n;
     }
     else
     {
          printf("parent, pid: %d \t ppid: %d \n", getpid(), getppid());
     }

     printf("n: %d \n", n);

     return 0;
}

输出:

process id: 7328 
test stdio buffer parent, pid: 7328 ppid: 4766 
n: 1 
test stdio buffer children, pid: 7329 ppid: 7328 
n: 2 

在fork()前调用的语句printf("test stdio buffer ")被执行了两次。这是因为,当标准输出是终端设备的时候,是行缓冲的,否则,是全缓冲。
当把"test stdio buffer "填入输出缓冲区的时候,并没有flush到终端,接下来子进程复制了这部分,因此会输出两次。
当句子改为:printf("test stdio buffer \n")的时候,仅输出一次,因为换行引起flush。


3 vfork function

vfork产生一次新进程,与fork不同的是:
-1:父子进程共享数据空间,在程序用例中有体现
-2: 子进程先运行,直到子进程调用exec或者退出后,父进程才会运行,如果子进程在运行过程中,需要等待父进程接下来运行的某个状态或者数值,则会死锁。

修改下fork的程序用例:

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

int main(int argc, char* argv[])
{
     int n = 1;

     printf("process id: %d \n", getpid());

     pid_t pid = vfork();
     if(pid < 0)
     {
          printf("fork failed, error[ %d ] \t [ %s ]\n", errno, strerror(errno));
          exit(EXIT_FAILURE);
     }
     
     if(pid == 0)
     {
          printf("children, pid: %d \t ppid: %d \n", getpid(), getppid());
          ++n;
          printf("n: %d \n", n);
          _exit(0);
     }
     else
     {
          printf("parent, pid: %d \t ppid: %d \n", getpid(), getppid());
          printf("n: %d \n", n);
     }

     exit(EXIT_SUCCESS);
}

输出:

process id: 7893 
children, pid: 7894 ppid: 7893 
n: 2 
parent, pid: 7893 ppid: 6718 
n: 2 

由于父子进程共享数据空间,因此,父进程中的n也为2(其实是同一个n)

需要注意的是,子进程中终止进程采用的是_exit(0)。由前面的章节得知,_exit并不会flush标准流。
如果子进程中,调用exit(0),而系统实现exit中会关闭标准I/O流,由于二者共享空间,那么父进程调用printf的时候,就不会产生输出。

不过,现在大多是的实现中,不会在exit中关闭标准I/O流,因为进程即将关闭,内核将关闭所有打开的文件描述符,因此,不必在exit中多此一举。所以,如果子进程中使用exit终止进程,编译运行,也会正常。


4 exit

exit相关已经在BOOKS: Advanced Programming in the UNIX Environment (6)第一节有所描述。

进程退出,释放所占用的资源,包括打开的文件描述符,申请的内存等,但是仍然保留了一定的信息,包括进程号,终止状态,运行时间等。直到父进程调用wait/waitpid来获取才会释放。

如果一个进程,大量产生子进程,等待子进程结束之后,缺不主动调用wait/waitpid去释放子进程的保留信息,就会造成不良后果,比如进程号被占用(系统所能使用的进程号是有限的),积累到一定程度后,会造成进程由于进程号限制而创建失败。

有一些退出的情况:

-1: 当父进程在子进程之前退出,即子进程失去了父亲,成为了孤儿进程,这时候,init进程就会收养这些孤儿进程,并且循环调用wait来释放已经推出的子进程。

-2: 当子进程先于父进程退出,释放一定的资源后,还会留下一些信息,此时的子进程称为僵尸进程。这些信息由父进程调用wait/waitpid后才会释放。子进程在退出的时候,会发送SIGCHLD信号给父进程,父进程接到信号后,在信号处理函数中调用wait/waitpid处理这些僵尸进程。默认情况下,父进程是忽略这些信号的。

程序用例在下一节中。


5 wait and waitpid functions

无论一个进程是正常结束还是异常终止,内核都会发送SIGCHLD信号给其父进程。子进程的终止是一个异步事件,可能发生在父进程运行的任何时候。内核通过信号异步通知父进程,父进程接到该信号后,可以选择忽略和处理。默认情况下是忽略该信号。父进程在信号处理函数中,可以调用以下两个函数之一来释放子进程退出后还保留的信息。

#include <sys/wait.h>

pid_t wait(int* stat_loc);
pid_t waitpid(pid_t pid, int* stat_loc, int options);

wait: 阻塞当前进程,直到有信号来或者子进程结束,如果在调用时子进程已经结束,则wait()会立即返回子进程的结束状态值。子进程的状态值由stat_loc返回。返回值为已结束的子进程ID。

waitpid: 阻塞当前进程,直到有信号来或者子进程结束,子进程状态值由stat_loc返回。

参数pid可选值:

< -1: 等待进程组识别码为pid绝对值的任何子进程
= -1: 等待任何子进程,相当于wait()
= 0 : 等待进程组识别码与当前进程相同的任何子进程
> 0 : 等待指定进程ID的子进程

参数options可选值:

0           : 

WCONTINUED  : 如果指定pid从作业控制暂停中继续,则返回。

WNOHANG     : 如果指定Pid的进程没有结束,也不阻塞,立即返回。

WUNTRACED   : 如果子进程进入暂停状态,则马上返回,不理会其结束状态。


对于返回的状态值,可以调用以下宏进行进一步判断(真,非零值):

WIFEXITED   : 如果为正常结束的子进程返回的状态,则为真

WEXITSTATUS : 对于正常结束的子进程,可以取得子进程退出状态的低八位

WIFSIGNALED : 如果是信号结束的子进程状态,则为真

WTERMSIG    : 对于信号结束的子进程,可以取得使子进程结束的信号编码

WIFSTOPPED  : 如果当前子进程是暂停而返回的,则为真

WSTOPSIG    : 对于暂停而返回的子进程,可以取得使子进程暂停的信号编码

WIFCONTINUED: 如果当前子进程是从一个作业控制暂停中继续的,则为真

程序用例1:父进程先退出

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h> // strerror

int main(int argc, char* argv[])
{
pid_t pid = fork();

if(pid < 0)
{
printf("fork failed. error[%d] \t %s \n", errno, strerror(errno));
exit(EXIT_FAILURE);
}

if(pid == 0)
{
printf("child process, pid: %d \t ppid: %d\n", getpid(), getppid());
sleep(5);
printf("child process, pid: %d \t ppid: %d\n", getpid(), getppid());
exit(EXIT_SUCCESS);
}
else
{
printf("father process\n");
printf("father exit\n");
}

exit(EXIT_SUCCESS);
}

输出:

father process
father exit
child process, pid: 10471   ppid: 10470
child process, pid: 10471   ppid: 1

父进程退出后,子进程被托管给了init(process ID为1)

程序用例2:子进程先退出,父进程不处理

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h> // strerror

int main(int argc, char* argv[])
{
pid_t pid = fork();

if(pid < 0)
{
printf("fork failed. error[%d] \t %s \n", errno, strerror(errno));
exit(EXIT_FAILURE);
}

if(pid == 0)
{
printf("child process, pid: %d \t ppid: %d\n", getpid(), getppid());
exit(EXIT_SUCCESS);
}
else
{
printf("father process, pid: %d\n", getpid());
sleep(10); // 方便查看僵尸进程
printf("father exit\n");
}

exit(EXIT_SUCCESS);
}

输出:

程序放后台运行,比如编译出程序为 test8.3, 则执行:./test8.3 &
当运行时,可以输入ps -l

[1] 10727
father process, pid: 10727
child process, pid: 10731   ppid: 10727
[alex_my@localhost Apue]$ ps -l
F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S  1000 10344  8837  0  80   0 - 29164 wait   pts/1    00:00:00 bash
0 S  1000 10727 10344  0  80   0 -  3122 hrtime pts/1    00:00:00 test8.3
1 Z  1000 10731 10727  0  80   0 -     0 exit   pts/1    00:00:00 test8.3 <defunct>
0 R  1000 10732 10344  0  80   0 - 30315 -      pts/1    00:00:00 ps

father exit

可以发现,子程序退出后,成为了僵尸进程,注意Z符号。

程序用例3:子进程先退出,父进程通过信号处理

clude <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h> // strerror
#include <signal.h>
#include <sys/wait.h>

// 信号处理函数
static void sig_process(int signo)
{
int status;
pid_t pid = wait(&status);
printf("child[%d] terminated\n", pid);
}

int main(int argc, char* argv[])
{
signal(SIGCHLD, sig_process);

pid_t pid = fork();
if(pid < 0)
{
printf("fork failed. error[%d] \t %s \n", errno, strerror(errno));
exit(EXIT_FAILURE);
}

if(pid == 0)
{
printf("child process, pid: %d \t ppid: %d\n", getpid(), getppid());
exit(EXIT_SUCCESS);
}
else
{
printf("father process, pid: %d\n", getpid());
sleep(10); // 等待子进程退出及ps -l
printf("father exit\n");
}

exit(EXIT_SUCCESS);
}


输出:

[1] 11013
father process, pid: 11013
child process, pid: 11017   ppid: 11013
child[11017] terminated
father exit
[1]+  Done                    ./test8.3
[alex_my@localhost Apue]$ ps -l
F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S  1000 10344  8837  0  80   0 - 29164 wait   pts/1    00:00:00 bash
0 R  1000 11018 10344  0  80   0 - 30315 -      pts/1    00:00:00 ps

可以看见,没有发现Z开头的僵尸进程了。当然,如果你够快,在子进程结束到父进程处理之前输入ps -l 还是可以发现的。
比如在把父进程退出时间改为20秒,在信号处理函数里边添加一句 sleep(10), 则在子进程退出后10秒内输入ps -l,还是能看见僵尸进程的。


6 waitid function

#include <sys/wait.h>

int waitid(idtype_t idtype, id_t id, siginfo_t* infop, int options);

如同waitpid, 但id意义依赖于idtype。

idtype可选值:

P_PID : id为指定子进程ID
P_PGID: 指定进程组ID,该组下的子进程均符合条件
P_ALL : 任意子进程, id无效

options与waitpid中的参数options相同。

infop结构包含:

si_pid   : process ID
si_uid   : real user ID or 0
si_signo : SIGCHLD
si_status: exit status or signal
si_code  : CLD_EXITED(_exit), CLD_KILLED(killed by signal), CLD_DUMPED(killed by signal, and dump core),
CLD_STOPPED(stopped by signal), CLD_TRAPPED(traced child has trapped), CLD_CONTINUED(child continued by SIGCONT)

程序用例:

将前边代码中的信号处理函数修改如下:

static void sig_process(int signo)
{
sleep(10);
int status;
siginfo_t info;

pid_t pid = waitid(P_ALL, 0, &info, 0);

printf("child process, pid: %d \t ppid: %d\n", getpid(), getppid());
printf("child[%d] terminated\n", pid);

printf("info: si_pid: %d \t si_uid: %d\n", info.si_pid, info.si_uid);
printf("info: si_code: %d\n", info.si_code);
}

然后运行,可以看到结果。




《unix环境高级编程》 读书笔记 (7)