第一部分 Linux进程概述
在Linux下进程被创建后可以有5种状态。
关于Linux进程状态不再赘述,参考下面这2个博客链接即可
不过有一些值得补充的说明:
-
很多操作系统教科书将正在CPU上执行的进程定义为RUNNING状态、而将可执行但是尚未被调度执行的进程定义为READY状态,这两种状态在linux下统一为 TASK_RUNNING状态。
-
进程状态变迁
进程自创建以后,状态可能发生一系列的变化,直到进程退出。而尽管进程状态有好几种,但是进程状态的变迁却只有两个方向——从TASK_RUNNING状态变为非TASK_RUNNING状态、或者从非TASK_RUNNING状态变为TASK_RUNNING状态。
(1)也就是说,如果给一个TASK_INTERRUPTIBLE状态的进程发送SIGKILL信号,这个进程将先被唤醒(进入TASK_RUNNING状态),然后再响应SIGKILL信号而退出(变为TASK_DEAD状态),并不会从TASK_INTERRUPTIBLE状态直接退出。进程从非TASK_RUNNING状态变为TASK_RUNNING状态,是由别的进程(也可能是中断处理程序)执行唤醒操作来实现的。执行唤醒的进程设置被唤醒进程的状态为TASK_RUNNING,然后将其task_struct结构加入到某个CPU的可执行队列中。于是被唤醒的进程将有机会被调度执行。
(2)而进程从TASK_RUNNING状态变为非TASK_RUNNING状态,则有两种途径:
a. 响应信号而进入TASK_STOPED状态、或TASK_DEAD状态;
b. 执行系统调用主动进入TASK_INTERRUPTIBLE状态 (如nanosleep系统调用)、或TASK_DEAD状态(如exit系统调用);或由于执行系统调用需要的资源得不到满足,而进入TASK_INTERRUPTIBLE状态或TASK_UNINTERRUPTIBLE状态(如select系统调用)。显然,这两种情况都只能发生在进程正在CPU上执行的情况下。 -
留意SIGSTOP、SIGCONT、SIGKILL信号对于进程状态的影响。另外,像pause()(等待信号的出现才被唤醒)、wait()(等待子进程退出)等函数都会使得当前进程进入可中断睡眠状态。
第二部分 Fork()详解
在Linux中手动创建一个新进程的唯一方法是使用fork()函数。
fork()函数用于从已存在的进程中创建一个新进程。新进程成为子进程,原进程成为父进程。子进程是父进程的一个复制品,它从父进程继承了进程的整个地址空间,包括进程上下文、代码段、进程堆栈、内存信息、打开的文件描述符、信号控制设定、进程优先级、进程组号、当前工作目录、根目录、资源限制和控制终端等。fork()出来的子进程除了进程标识符pid和父进程不一样以外,其他的都和父进程相同。下面这张图形象的描述了进程的地址空间。
关于fork()需要注意以下几点
- 父进程中的返回值是子进程的进程号, 而在子进程中返回0,因此可以通过返回值来判定当前进程是父进程还是子进程。当一个父进程创建多个子进程时(注意此时fork()一定要保证在父进程运行状态下被调用),在代码的不同调用位置可以拿到不同的子进程pid,可参见以下代码,注意留意父进程创建多个子进程的fork()调用位置。
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
int main(){
pid_t pid;
pid = fork();
if(pid < 0){
printf("Fork1 error!\n");
exit(1);
}
if(pid > 0){
printf("the father(1) is running, pid is %d, ret value is %d\n", getpid(), pid);
pid_t pid2 = fork();
if(pid2 < 0){
printf("Fork2 error!\n");
exit(1);
}
if(pid2 > 0){
printf("the father(2) is running, pid is %d, ret value is %d\n", getpid(), pid2);
exit(0);
}else if(pid2 == 0){
printf("the second son is running, pid is %d, ret value is %d\n", getpid(), pid2);
exit(0);
}
}else if(pid == 0){
printf("the first son is running, pid is %d, ret value is %d\n", getpid(), pid);
exit(0);
}
return 0;
}
运行效果
第三部分 wait()/waitpid()详解
(1)wait()和waitpid()区别
- wait()用于使父进程阻塞等待,直到一个子进程结束或该进程接到了一个信号为止。当该父进程没有子进程或者是子进程结束时会立即返回。
- waitpid()的作用与wait()相似,但是不一定要等待第一个结束的子进程,它有若干选项,比wait()更加灵活,比如提供一个非阻塞等待的版本。
(2)wait()函数
函数原型 | pid_t wait(int *status); |
---|---|
所需头文件 | #include<sys/types.h>和#include<sys/wait.h> |
成功 | 返回已结束运行的子进程的进程号 |
失败 | 返回-1 |
参数说明
- 参数status如果不是一个空指针,则终止进程的终止状态就存放在statloc所指向的单元;
- 参数status如果是一个空指针,则表示父进程不关心子进程的终止状态。
(3) waitpid()函数
函数原型 | pid_t waitpid(pid_t pid, int *status, int options); |
---|---|
所需头文件 | #include<sys/types.h>和#include<sys/wait.h> |
成功 | 返回已结束运行的子进程的进程号 |
options为WNOHANG时 | 没有子进程退出则立即返回0 |
失败 | 返回-1 |
参数说明
- pid
pid>0 | 只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。 |
---|---|
pid=-1 | 等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。 |
pid=0 | 等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。 |
pid<-1 | 等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。 |
- options
WNOHANG | 若由pid指定的子进程未发生状态改变(没有结束),则waitpid()不阻塞,立即返回0 |
---|---|
WUNTRACED | 返回终止子进程信息和因信号停止的子进程信息 |
WCONTINUED | 返回收到SIGCONT信号而恢复执行的已停止子进程状态信息 |
0 | 这个时候跟wait()一样阻塞等待 |
- status
同wait()
用法
- 一个常见的用法是
waitpid(pid, NULL, 0)
,这样就可以阻塞等待进程号为pid的子进程退出了。注意这个用法和wait()
是不一样的,后者只能等待任意一个子进程退出,而waitpid()
可以指定等待具体哪个进程。
第四部分 关于返回值pid_t的补充说明
- pid_t在头文件types.h(sys/types.h)中定义。
- pid_t是一个typedef定义类型。
用它来表示进程id类型。
//sys/types.h:
typedef short pid_t; /* used for process ids */
由此看出,上述定义的pid_t就是一个short类型变量,实际表示的是内核中的进程表的进程号索引。
- 使用pid_t的一个好处——可移植性更好
比如对于不同的平台,可以typedef int pid_t
,也可以typedef long pid_t
- 事实上Linux内核源码的数据类型基本都把基本类型进行了一次
typedef
的封装。
参考文献
https://blog.csdn.net/csdn_kou/article/details/81091191
《嵌入式Linux应用开发标准教程》 华清远见嵌入式培训中心 编著
《深入理解计算机系统》 Randal E.Bryant, David R.O’Hallaron编著