Linux进程控制
本文介绍了Linux系统中的进程控制机制,主要包括:1. 进程标识与创建:通过getpid/getppid获取进程ID,使用fork创建子进程;2. 进程状态:运行、停止和终止三种状态;3. 进程终止方式:信号终止、主程序返回或调用exit;4. 僵尸进程问题:产生原因及两种清除方法(父进程回收或杀死父进程);5. 进程回收:wait/waitpid函数的使用方法和参数说明;6. 进程休眠:sle
进程控制
原文链接:https://kidwjb.top/archives/149
获取进程ID
每个进程都有一个唯一的非零正数进程ID(PID)
-
getpid函数返回调用进程的PID -
getppid函数返回他的父进程的PID
#include <sys/types.h>
#include <unistd.h
pid_t getpid(void);
pid_t getppid(void);
创建和终止进程
从程序员的角度,我们可以认为进程总是处于下面三种状态之一:
-
运行:进程要么在CPU上执行,要么在等待被执行且最终会被内核调度。
-
停止:进程的执行被桂起(suspended),且不会被调度。当收到
SIGSTOP、SIGTSTP、SIGTTIN或者SIGTTOU信号时,进程就停止,并且保持停止直到它收到一个SIGCONT信号,在这个时刻,进程再次开始运行 -
终止:进程永远地停止了。进程会因为三种原因终止:
-
收到一个信号,该信号的默认行为是终止进程
-
从主程序返回
-
调用exit函数。
-
exit
#include <stdlib.h>
void exit(int status);
exit函数以status退出状态来终止进程(另一种设置退出状态的方法是从主程序中返回一个整数值)。
fork
父进程通过调用fork函数创建一个新的运行的子进程。
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
新创建的子进程几乎但不完全与父进程相同:
-
子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈
-
子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件
-
父进程和新创建的子进程之间最大的区别在于它们有不同的PID
fork函数只被调用一次,但是却返回两次:一次是在调用进程(父进程)中,一次是在新创建的子进程中。
-
在父进程中,fork返回子进程的PID。
-
在子进程中,fork返回0。因为子进程的PID总是为非零,返回值就提供一个明确的方法来分辨程序是在父进程还是在子进程中执行
示例代码如下:
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char **argv)
{
if(fork() == 0)
{
printf("p1:x = %d\r\n",getpid());
exit(0);
}
printf("p2:x = %d\r\n",getpid());
exit(0);
}
编译运行得到如下结果:
p2:x = 9114
p1:x = 9115
这里显示了几个有趣的点:
-
调用一次,返回两次。fork函数被父进程调用一次,但是却返回两次--一次是返回到父进程,一次是返回到新创建的子进程。
-
并发执行。父进程和子进程是并发运行的独立进程。内核能够以任意方式交替执行它们的逻辑控制流中的指令。在我们的系统上运行这个程序时,父进程先完成它的printf语句,然后是子进程。然而,在另一个系统上可能正好相反。
-
相同但是独立的地址空间。如果能够在fork函数在父进程和子进程中返回后立即暂停这两个进程,我们会看到两个进程的地址空间都是相同的。每个进程有相同的用户栈、相同的本地变量值、相同的堆、相同的全局变量值,以及相同的代码。
-
共享文件。当运行这个示例程序时,我们注意到父进程和子进程都把它们的输出显示在屏幕上。原因是子进程继承了父进程所有的打开文件。当父进程调用fork时,stdout文件是打开的,并指向屏幕。子进程继承了这个文件,因此它的输出也是指向屏幕的
回收子进程
当一个进程由于某种原因终止时,内核并不是立即把它从系统中清除。相反,进程被保持在一种已终止的状态中,直到被它的父进程回收(reaped)。当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃已终止的进程,从此时开始,该进程就不存在了。一个终止了但还未被回收的进程称为僵死进程(zombie)。
僵死进程
简介
在fork()/execve()过程中,假设子进程结束时父进程仍存在,而父进程fork()之前既没安装SIGCHLD信号处理函数调用waitpid()等待子进程结束,又没有显式忽略该信号,则子进程成为僵尸进程,无法正常结束,此时即使是root身份kill -9也不能杀死僵尸进程。补救办法是杀死僵尸进程的父进程(僵尸进程的父进程必然存在),僵尸进程成为"孤儿进程",过继给1号进程init,init始终会负责清理僵尸进程。
怎么查看僵尸进程:
利用命令ps,可以看到有标记为Z的进程就是僵尸进程。
产生
一个进程在调用exit命令结束自己的生命的时候,其实它并没有真正的被销毁,而是留下一个称为**僵尸进程(Zombie)**的数据结构(系统调用exit,它的作用是使进程退出,但也仅仅限于将一个正常的进程变成一个僵尸进程,并不能将其完全销毁)。在Linux进程的状态中,**僵尸进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。**它需要它的父进程来 为它收尸,如果他的父进程没安装SIGCHLD信号处理函数调用wait或waitpid()等待子进程结束,又没有显式忽略该信号,那么它就一直保持僵 尸状态,如果这时父进程结束了,那么init进程自动会接手这个子进程,为它收尸,它还是能被清除的。但是如果父进程是一个循环,不会结束,那么子进程就 会一直保持僵尸状态,这就是为什么系统中有时会有很多的僵尸进程。
清除
1.改写父进程, 在子进程死后要为它收尸。具体做法是接管SIGCHLD信号。子进程死后,会发送SIGCHLD信号给父进程,父进程收到此信号后,执行 waitpid()函数为子进程收尸。这是基于这样的原理:就算父进程没有调用wait,内核也会向它发送SIGCHLD消息,尽管对的默认处理是忽略, 如果想响应这个消息,可以设置一个处理函数。
2.把父进程杀掉。父进程死后,僵尸进程成为"孤儿进程",过继给1号进程init,init始终会负责清理僵尸进程.它产生的所有僵尸进程也跟着消失。
Linux中也可使用这个,在一个程序的开始调用这个函数
signal(SIGCHLD,SIG_IGN);
zombie进程是僵死进程。防止它的办法,一是用wait,waitpid之类的函数获得
进程的终止状态,以释放资源。另一个是fork两次
defunct进程只是在process table里还有一个记录,其他的资源没有占用,除非你的系统的process个数的限制已经快超过了,zombie进程不会有更多的坏处。
在Unix下的一些进程的运作方式。当一个进程死亡时,它并不是完全的消失了。进程终止,它不再运行,但是还有一些残留的小东西等待父进程收回。这些残留的东西包括子进程的返回值和其他的一些东西。当父进程 fork() 一个子进程后,它必须用 wait() 或者 waitpid() 等待子进程退出。正是这个 wait() 动作来让子进程的残留物消失。
自然的,在上述规则之外有个例外:父进程可以忽略 SIGCLD 软中断而不必要 wait()。可以这样做到(在支持它的系统上,比如Linux)
waitpid
一个进程可以通过调用waitpid函数来等待它的子进程终止或被停止
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *statusp, int options);
waitpid函数有点复杂。
-
默认情况下(当options=0时),
waitpid挂起调用进程的执行,直到它的等待集合(wait set)中的一个子进程终止。 -
如果等待集合中的一个进程在刚调用的时刻就已经终止了,那么
waitpid就立即返回。 -
在这两种情况中,waitpid返回导致waitpid返回的已终止子进程的PID。此时,已终止的子进程已经被回收,内核会从系统中删除掉它的所有痕迹。
判定等待集合的成员
等待集合的成员是由参数pid确定的:
-
如果
pid>0,那么等待集合就是一个单独的子进程,它的进程ID等于pid。 -
如果
pid=-1,那么等待集合就是由父进程所有的子进程组成的。
修改默认行为
可以通过将 options设置为常量WNOHANG、WUNTRACED 和 WCONTINUED的各种组合来修改默认行为:
-
WNOHANG:如果等待集合中的任何子进程都还没有终止,那么就立即返回(返回值为0)。默认的行为是挂起调用进程,直到有子进程终止。在等待子进程终止的同时,如果还想做些有用的工作,这个选项会有用 -
WUNTRACED:挂起调用进程的执行,直到等待集合中的一个进程变成已终止或者被停止。返回的PID为导致返回的已终止或被停止子进程的PID。默认的行为是只返回已终止的子进程。当你想要检查已终止和被停止的子进程时,这个选项会有用 -
WCONTINUED:挂起调用进程的执行,直到等待集合中一个正在运行的进程终止或等待集合中一个被停止的进程收到SIGCONT信号重新开始执行。
可以用或运算把这些选项组合起来。例如:WNOHANG|WUNTRACED:立即返回,如果等待集合中的子进程都没有被停止或终止,则返回值为0;如果有一个停止或终止,则返回值为该子进程的PID。
检查已回收子进程的退出状态
如果statusp参数是非空的,那么waitpid就会在status中放上关于导致返回的子进程的状态信息,status是statusp指向的值。wait.h头文件定义了解释status数的几个宏:
-
WIFEXITED(status):如果子进程通过调用 exit或者一个返回(return)正常终止,就返回真。 -
WEXITSTATUS(status):返回一个正常终止的子进程的退出状态。只有在WIFEXITED()返回为真时,才会定义这个状态。 -
WIFSIGNALED(status):如果子进程是因为一个未被捕获的信号终止的,那么就返回真。 -
WIFSTOPPED(status):如果引起返回的子进程当前是停止的,那么就返回真。 -
WSTOPSIG(status):返回引起子进程停止的信号的编号。只有在WIFSTOPPED()返回为真时,才定义这个状态。 -
WIFCONTINUED(status):如果子进程收到SIGCONT信号重新启动,则返回真。
错误条件
如果调用进程没有子进程,那么waitpid返回一1,并且设置errno为ECHILD。 如果waitpid函数被一个信号中断,那么它返回一1,并设置errno为EINTR
wait函数
wait函数是waitpid的简化版本
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *statusp);
调用wait(&status)等价于调用waitpid(- 1,&status,0)。
示例
示例1:
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(int argc,char **argv)
{
if(fork() == 0)
{
fprintf(stdout,"a\r\n");
}
else
{
fprintf(stdout,"b\r\n");
waitpid(-1,NULL,0);
}
fprintf(stdout,"c\r\n");
exit(0);
}
输出如下:
b
a
c
c
示例2:
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <errno.h>
#define CHILDNUM 3
int main(int argc,char **argv)
{
int status;
int i = 0;
pid_t pid;
//先创建CHILDNUM个子进程
for(i = 0;i < CHILDNUM;i++)
{
if((pid = fork()) == 0) //子进程
{
exit(i);
}
}
//父进程等待子进程终止
while((pid = waitpid(-1,&status,0)) > 0)
{
if(WIFEXITED(status))
{
printf("child %d exit with status = %d\r\n",pid,WEXITSTATUS(status));
}
else
{
printf("child %d exit abnormally\r\n",pid);
}
}
if(errno != ECHILD)
{
printf("waitpid error\r\n");
}
exit(0);
}
输出结果如下:
child 9297 exit with status = 0
child 9298 exit with status = 1
child 9299 exit with status = 2
注意,程序不会按照特定的顺序回收子进程。子进程回收的顺序是这台特定的计算机系统的属性。在另一个系统上,甚至在同一个系统上再执行一次,两个子进程都可能以相反的顺序被回收。
进程休眠
sleep函数将一个进程挂起一段指定的时间
#include <unistd.h>
unsigned int sleep(unsigned int secs);
如果请求的时间量已经到了,sleep返回0,否则返回还剩下的要休眠的秒数。后一种情况是可能的,如果因为s1eep函数被一个信号中断而过早地返回。 另一个很有用的函数是pause函数,该函数让调用函数休眠,直到该进程收到一个信号。
#include <unistd.h>
int pause(void)
加载并运行程序
execve函数在当前进程的上下文中加载并运行一个新程序
#include <unistd.h>
int execve(const char *filename, const char *argv[],const char *envp[]);
execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。所以,与fork一次调用返回两次不同,execve调用一次并从不返回。
argv变量指向一个以null结尾的指针数组,其中每个指针都指向一个参数字符串。按照惯例,argv[0]是可执行目标文件的名字。环境变量的列表是由一个类似的数据结构表示的,envp变量指向一个以null结尾的指针数组,其中每个指针指向一个环境变量字符串,每个串都是形如name=value的名字-值对。
在execve加载了filename之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,该主函数有如下形式的原型
int main(int argc, char **argv, char **envp);
当main开始执行时,用户栈的组织结构如图所示:

首先是参数和环境字符串。栈往上紧随其后的是以null结尾的指针数组,其中每个指针都指向栈中的一个环境变量字符串。全局变量environ指向这些指针中的第一个envp[0]。紧随环境变量数组之后的是以null结尾的argv[]数组,其中每个元素都指向栈中的一个参数字符串。在栈的顶部是系统启动函数libc_start_main的栈帧。
Linux提供了几个函数来操作环境数组:
#include <stdlib.h>
char *getenv(const char *name);
getenv函数在环境数组中搜索字符串“name=value”。如果找到了,它就返回一个指向value的指针,否则它就返回NULL。
#include <stdlib.h>
int setenv (const char *name, const char *newvalue, int overwrite);
void unsetenv(const char *name);
如果环境数组包含一个形如“name=oldvalue”的字符串,那么unsetenv会删除它,而 setenv 会用newvalue 代替 oldvalue,但是只有在 overwirte非零时才会这样。如果 name 不存在,那么 setenv就把“name=newvalue”添加到数组中。
程序和进程
程序是一堆代码和数据;
-
程序可以作为目标文件存在于磁盘上,或者作为段存在于地址空间中。
-
进程是执行中程序的一个具体的实例;
-
程序总是运行在某个进程的上下文中。
-
fork函数在新的子进程中运行相同的程序,新的子进程是父进程的一个复制品。 -
execve函数在当前进程的上下文中加载并运行一个新的程序。它会覆盖当前进程的地址空间,但并没有创建一个新进程。新的程序仍然有相同的PID,并且继承了调用execve函数时已打开的所有文件描述符。
更多推荐



所有评论(0)