进程标识
每一个进程都有一个非负整数表示的唯一进程ID。ID为0的进程通常是调度进程,是内核的一部分。进程ID为1的进程通常是init进程,负责在自举内核后启动一个UNIX系统。init通常读取与系统相关的初始化文件(etc/rc*等)。init进程不会终止,是一个以超级用户权限运行的普通用户程序,不是内核中的系统进程。init会成为所有孤儿进程的父进程。
函数fork
#include <unistd.h>
pid_t fork(void);
由fork创建的进程被称为子进程。fork被调用一次,但是返回两次。子进程中返回0,父进程中则返回子进程的进程ID。子进程和父进程会继续执行fork之后的代码,子进程是父进程的副本,子进程会获得父进程的数据空间,堆和栈。注意是父进程的副本,而不是共享。
使用fork的例子:
if( (pid=fork())<0){//此处调用一个fork
//返回值小于0,出错啦
}else if(pid == 0){
//此处在子进程中
doChildsWork();、
//exit(0); //这个代码使子进程结束。
}else{
//此处在父进程中
doParentsWork();
}
doBothTodo();//此处代码父子进程都会执行。
函数wait和waitpid
当一个进程正常终止或异常终止时,内核就向其父进程发送SIGCHLD信号,父进程可以选择忽略此信号,或者提供一个该信号发生即被执行的信号处理函数。对于这种信号,系统的默认操作是忽略。
这里讲一下“僵死进程”:
在类UNIX系统中,僵尸进程是指完成执行(通过exit系统调用,或运行时发生致命错误或收到终止信号所致)但在操作系统的进程表中仍然有一个表项(进程控制块PCB),处于”终止状态”的进程。这发生于子进程需要保留表项以允许其父进程读取子进程的exit status:一旦退出态通过wait系统调用读取,僵尸进程条目就从进程表中删除,称之为”回收(reaped)”。但如果父进程没有调用wait,僵尸进程将保留进程表中的表项,导致了资源泄漏。
如果编写一个长期运行的程序,它fork了很多子程序,那么除非父进程等待取得子程序的终止状态,不然这些子进程终止后就会变成僵死进程。
下面给出wait和waitpid的声明:
#include <sys/wait.h>
pid wait(int *statloc);
pid waitpid9pid_t pid,int *statloc,int options);
调用wait和waitpid会发生:
- 如果其所有子进程都在运行,则阻塞
- 如果一个子进程已终止,正等待父进程获取其终止状态,则取得终止状态,立即返回。
- 如果没有任何子进程,则立即出错返回。
如果进程因为接收到SIGCHLD信号而调用wait(信号处理函数),我们期待wait立即返回。但是如果在随即时间点调用wait,则进程可能会阻塞。
另外,这两个函数存在一些区别:
- 在一个子程序结束前,wait使其调用者阻塞,而waitpid有一选项,可是调用者不阻塞。
- waitpid并不等待其调用之后的第一个终止子程序,它有若干个选项可以控制它所等待的进程。
这两个函数的参数statloc是一个整型指针。如果statloc不是一个空指针,则终止进程的终止状态就存放在它所指向的单元内。如果不关心终止状态,则可将该参数设为空指针。
对于waitpid函数,pid参数的作用如下。
- pid=-1,等待任意子程序,此时与wait相同
- pid>0,等待进程ID与pid相同的子程序
- pid=0,等待组ID和调用进程组ID的任意子程序
- pid<-1,等待组ID等于pid绝对值的任一子程序
阅读unix网络编程之后发现一种处理结束的子进程防止僵死进程的方式:
signal(SIGCHLD,sig_chld);
void sig_chld(int signo){
int statloc;
pid_t pid;
/**
* 下面的while循环中
* 使用了waitypid函数,第一个参数-1表示任意wait子进程,与wait相同
* 第二个参数用于保存进程终止状态
* 第三个参数是option,WNOHANG代表如果没有终止的子进程,不阻塞而是直接返回。
* 说一下使用while+WNOHANG的原因:
* unix中信号是不排队的,也就是说,如果在SIGCHLD信号处理的过程中,有其他子进程结束,产生SIGCHLD信号。
* 新的SIGCHLD信号是不递交的(也就是说,即使有多个子进程结束,即产生多个SIGCHLD,也只会处理一次)。
* 所以,我们需要在一次信号处理中,肯能需要处理多个终止子程序,因此使用while
* 至于WNOHANG选项,如果不加上,就阻塞在信号处理程序了。
*/
while((pid=waitpid(-1,&statloc,WNOHANG))>0){
//todo
//信号处理函数不应该使用printf这类不可重入函数
printf("子进程%d结束",pid);
}
return;
}
函数exec
当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序从其main函数处开始执行。因为调用exec并不创建新进程所以前后进程ID没有变化。
#include <unist.h>
/* Execute the file FD refers to, overlaying the running program image.
ARGV and ENVP are passed to the new program, as for `execve'. */
extern int fexecve (int __fd, char *const __argv[], char *const __envp[])
/* Execute PATH with arguments ARGV and environment from `environ'. */
extern int execv (const char *__path, char *const __argv[])
/* Execute PATH with all arguments after PATH until a NULL pointer,
and the argument after that for environment. */
extern int execle (const char *__path, const char *__arg, ...)
/* Execute PATH with all arguments after PATH until
a NULL pointer and environment from `environ'. */
extern int execl (const char *__path, const char *__arg, ...)
/* Execute FILE, searching in the `PATH' environment variable if it contains
no slashes, with arguments ARGV and environment from `environ'. */
extern int execvp (const char *__file, char *const __argv[])
/* Execute FILE, searching in the `PATH' environment variable if
it contains no slashes, with all arguments after FILE until a
NULL pointer and environment from `environ'. */
extern int execlp (const char *__file, const char *__arg, ...)