process control
1 process identifiers
2 fork function
3 vfork function
4 exit
5 wait and waitpid functions
6 waitid function
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) 第三节,链接:
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);
}
然后运行,可以看到结果。