Linux系统编程(五):多进程和fork()

1 进程和PCB控制块

1.1 进程简单描述

进程可以理解为程序的一次执行,也可以理解为程序运行的一个实例。进程是分配资源的基本单位。一个进程由三部分组成:进程控制块PCB(Process Control Block),有关程序段,该程序段对其操作的数据结构集。

每个用户均可同时运行多个程序。为了区分每一个运行的程序,Linux给每个进程都做了标识,称为进程号PID(process ID),每个进程的进程号是唯一的。

Linux 给每个进程都打上了运行者的标志,用户可以控制自己的进程:给自己的进程分配不同的优先级,也可以随时终止自己的进程。其实就是进程继承了执行者的UID和GID。

Linux 不可能在一个 CPU 上同时处理多个任务(作业)请求,而是采用 “分时” 技术来处理这些任务请求。

1.2 ps和kill命令

常使用ps命令来查看当前运行的进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 使用ps ajx 可以查看其父进程
$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 8892 228 ? Ssl 13:13 0:00 /init
root 8 0.0 0.0 8904 160 tty1 Ss 13:13 0:00 /init
choupin 9 0.0 0.0 8436 100 tty1 S 13:13 0:03 /mnt/e/wsl-term
choupin 11 0.0 0.0 22296 7476 pts/0 Ss 13:13 0:19 /bin/zsh
root 46 0.0 0.0 8908 164 tty2 Ss 13:13 0:00 /init
choupin 47 0.0 0.0 10660 476 tty2 S 13:13 0:00 sh -c "$VSCODE_
choupin 48 0.0 0.0 10660 520 tty2 S 13:13 0:00 sh /mnt/c/Users
choupin 53 0.0 0.0 10660 500 tty2 S 13:13 0:00 sh /home/choupi
choupin 55 0.0 0.2 979712 49872 tty2 Sl 13:13 0:25 /home/choupin/.
choupin 75 0.0 0.1 861412 20436 tty2 Sl 13:13 0:00 /home/choupin/.
choupin 82 0.0 0.1 791204 19028 tty2 Sl 13:13 0:00 /home/choupin/.
choupin 113 0.0 0.1 860912 19612 tty2 Sl 13:13 0:00 /home/choupin/.

STAT字段表示进程状态,在下一节介绍。详细命令参数可通过man手册查看。

可以使用kill命令通过进程PID号来终止进程

1
kill 9527 -9

其中-9是终止的信号参数。

1.2 进程的状态

一个进程至少具有5种基本状态:初始态、执行状态、等待(阻塞)状态、就绪状态、终止状态。

  • 初始状态:进程刚被创建,由于其他进程正占有CPU所以得不到执行,只能处于初始状态。
  • 就绪状态:只有处于就绪状态的经过调度才能到执行状态。
  • 运行状态:任意时刻处于执行状态的进程只能有一个。
  • 等待状态:进程等待某件事件完成
  • 终止状态:进程结束

有的系统,为了暂时缓和内存的紧张状态,或为了调节系统负荷,又引入了挂起的功能:暂时挂起一部分进程,把他们从内存临时换出到外存。

阻塞挂起:进程在外存并等待某事件的出现

就绪挂起:进程在外存,但只要进入内存,就可以运行

事实上不同系统不同翻译中对进程状态的描述略有差别。其中STAT字段即表示进程状态,通过man ps可以查看相关含义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
D    uninterruptible sleep (usually IO)
R running or runnable (on run queue)
S interruptible sleep (waiting for an event to complete)
T stopped by job control signal
t stopped by debugger during the tracing
W paging (not valid since the 2.6.xx kernel)
X dead (should never be seen)
Z defunct ("zombie") process, terminated but not reaped by its parent

< high-priority (not nice to other users)
N low-priority (nice to other users)
L has pages locked into memory (for real-time and custom IO)
s is a session leader
l is multi-threaded (using CLONE_THREAD, like NPTL pthreads do)

对应解释为

  1. R—TASK_RUNNING(可执行状态)
  2. S—TASK_INTERRUPTIBLE(可中断的睡眠状态)
  3. D—TASK_UNINTERRUPTIBLE(不可中断的睡眠状态)
  4. T — TASK_STOPPED或TASK_TRACED(暂停状态或跟踪状态)
  5. Z — TASK_DEAD - EXIT_ZOMBIE(退出状态,进程成为僵尸进程)
  6. X — TASK_DEAD - EXIT_DEAD(退出状态,进程即将被销毁)

详细介绍可见这里。

1.3 PCB进程控制块

在Linux中进程信息存放在叫做进程控制块的数据结构中,每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息。Linux内核的进程控制块是task_struct结构体。这在不同系统中的实现略有差别。

task_struct是Linux内核的一种数据结构,它会被装载到RAM并且包含着进程的信息。每个进程都把它的信息放在task_struct这个数据结构体中,其中包含了众多信息。

主要信息有

  • 进程id

  • 进程状态status

  • 文件描述符表

  • 进程工作目录位置

  • umask掩码

  • 信号相关信息资源

  • 用户id和组id

详细内容见操作系统相关书籍,留待后续补充。

2 fork()和父子进程

2.1 fork()

可以在程序中通过fork()来创建进程,此时创建的进程即是该程序进程的子进程。

1
2
#include <unistd.h>
pid_t fork(void);

这个函数非常奇妙,在正常执行的情况下,将会有两个返回值。所谓的两个返回值其实就是在父子进程中各返回一次,在父进程中返回的是子进程的PID,而在子进程中返回的是0,因此可以通过检查返回值,来确定后续代码在父进程还是子进程中执行。因为进程号都是大于0的,所以执行出错时返回-1。

2.2 getpid()\getppid()

这两个函数分别可以获取当前进程的PID和当前进程父进程的PID。

1
2
3
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);

该函数不会执行出错,无需判断返回值。

2.3 父子进程关系

在刚刚完成fork()后,父子进程间的data段、text段、堆、栈、环境变量、全局变量、宿主目录位置、进程工作目录位置、信号处理方式都完全一样。

而进程PID、fork()返回值、各自父进程、进程创建时间、闹钟、未决信号集则不一样。

而其相同的全局变量也并未完全是简单地拷贝,而是遵循“读时共享、写时复制”的原则。如文件描述符和mmap映射区。

2.4 循环创建n个子进程

这里要求循环创建n个子进程时,并且保证所有子进程均由最初父进程产生,也就是说所有子进程应该是兄弟关系。并可以控制特定子进程完成特定操作。

fork_loop.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

int main() {
pid_t pid;
int i;
for (i = 0; i < 5; ++i) {
if ((pid = fork()) == 0)
break;
}
if (i == 5) {
sleep(5);
printf("parent, and my PID is %d\n", getpid());
} else {
sleep(i);
printf("%dth child, and my PID is %d, my parent's PID is %d\n", i + 1,
getpid(), getppid());
}
return 0;
}

编译执行,看看效果

1
2
3
4
5
6
7
$ ./fork_loop
1th child, and my PID is 3669, my parent's PID is 3668
2th child, and my PID is 3670, my parent's PID is 3668
3th child, and my PID is 3671, my parent's PID is 3668
4th child, and my PID is 3672, my parent's PID is 3668
5th child, and my PID is 3673, my parent's PID is 3668
parent, and my PID is 3668

3 wait()和waitpid()

3.1 函数介绍

先介绍一下孤儿进程和僵尸进程。

  • 孤儿进程:父进程先于子进终止,子进程沦为“孤儿进程”,会被 init 进程领养。
  • 僵尸进程:子进程终止,父进程尚未对子进程进行回收,在此期间,子进程为“僵尸进程”。kill 对其无效。子进程的PCB资源会残留在内核中,浪费内存资源。

所以在子进程终止之后,父进程应当承担回收子进程的责任,否则就可能产生故而进程和僵尸进程。

可以在父进程中使用wait()waitpid()来对子进程进行回收。

1
2
3
4
5
6
#include <sys/wait.h>
// 参数为一个传出参数,表示回收子进程的状态
// 可传入NULL,表示不关心子进程终止状态
pid_t wait(int *wstatus);
// 参数1指定要回收的子进程,参数3可设置WNOHANG非阻塞
pid_t waitpid(pid_t pid, int *wstatus, int options);

wait()函数将阻塞地回收任意一个子进程;执行成功时将返回回收进程的PID,失败返回-1;通过传出参数wstatus可以获得子进程终止状态。

  • 获取子进程正常终止值:WIFEXITED(status)为真的话,可调用 WEXITSTATUS(status)获得子进程的退出值。
  • 获取导致子进程异常终止信号:WIFSIGNALED(status)为真的话,可调用WTERMSIG(status)得到导致子进程异常终止的信号编号。

waitpid()则可以指定特定子进程进行回收,同时也可以设置非阻塞。返回值大于0时表示成功回收子进程,返回值就是对应PID;返回值为0时,表示设置了非阻塞且子进程未结束未能回收;返回值为-1时表程序执行出错。

注意无论这两个函数一次都只能回收一个子进程,需要回收多个子进程需要使用循环。

3.2 使用实例

在2.4节的程序中,子进程先于父进程终止,且父进程未对其回收,所以会产生僵尸进程。这里对其进行改进

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>

#include "../helper.h"

int main() {
pid_t pid, wpid;
int status, i;
for (i = 0; i < 5; ++i) {
if ((pid = fork()) == -1)
sys_err("fork error");
if (pid == 0)
break;
}
// 子进程
if (i < 5) {
printf("It is a child process %d, and the parent process id is %d\n",
getpid(), getppid());
sleep(5);
printf("the child process going to die\n");
return i;
} else { // 父进程
while(i--){
wpid = wait(&status);
if (wpid == -1)
sys_err("wait error");
printf("It is a parent process %d\n", getpid());
if (WIFEXITED(status))
printf("child exit with %d\n", WEXITSTATUS(status));
if (WIFSIGNALED(status))
printf("child kill with signal %d\n", WTERMSIG(status));
}
}
return 0;
}

编译后执行,得到结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ ./wait
It is a child process 3938, and the parent process id is 3937
It is a child process 3939, and the parent process id is 3937
It is a child process 3940, and the parent process id is 3937
It is a child process 3941, and the parent process id is 3937
It is a child process 3942, and the parent process id is 3937
the child process going to die
the child process going to die
It is a parent process 3937
child exit with 0
the child process going to die
It is a parent process 3937
child exit with 1
the child process going to die
It is a parent process 3937
child exit with 2
It is a parent process 3937
child exit with 3
the child process going to die
It is a parent process 3937
child exit with 4

显然,所有子进程都已经被回收。

4 结束语

本篇文章先简单介绍了进程和PCB控制块的概念,因为这其实应该是操作系统的相关概念,所以在此不多做纠缠,只是有个简单印象,同时记录了相关常用命令pskill;然后介绍了用来创建子进程的fork()函数,已经实现了一个循环创建特定个数子进程的小demo;最后介绍了用以回收子进程的wait()waitpid()函数,并对之前的小demo做了改进,使其不再产生僵尸进程。

fork()函数非常重要,在后续文章其他函数的学习中,还将多次使用其实现多进程程序的编写。

-------- 本文结束 感谢阅读 --------
给我加块红烧肉吧
  • 本文标题: Linux系统编程(五):多进程和fork()
  • 本文作者: Chou Bin
  • 创建时间: 2020年02月24日 - 01时02分
  • 修改时间: 2020年02月24日 - 16时02分
  • 本文链接: http://yoursite.com/2020/02/24/apue-hm05/
  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!