Linux 多进程开发(1)

Linux 多进程开发(1)

进程相关命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 1
# man ps # 查看手册
# ps - report a snapshot of the current processes.
ps [-option] # aux/ajx
a: all
u:
j: job
x:

# 2
tty # 查看当前终端信息

# 3 实时查看进行信息
top [-option]

# 4 向进程发送信号
kill
kill -l # 查看所有信号

# 后台运行
[命令] &

进程的创建

fork函数:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/*
#include <sys/types.h>
#include <unistd.h>

pid_t fork(void);
函数的作用:用于创建子进程。
返回值:
fork()的返回值会返回两次。一次是在父进程中,一次是在子进程中。
在父进程中返回创建的子进程的ID,
在子进程中返回0
如何区分父进程和子进程:通过fork的返回值。
在父进程中返回-1,表示创建子进程失败,并且设置errno

父子进程之间的关系:
区别:
1.fork()函数的返回值不同
父进程中: >0 返回的子进程的ID
子进程中: =0
2.pcb中的一些数据
当前的进程的id pid
当前的进程的父进程的id ppid
信号集

共同点:
某些状态下:子进程刚被创建出来,还没有执行任何的写数据的操作
- 用户区的数据
- 文件描述符表

父子进程对变量是不是共享的?
- 刚开始的时候,是一样的,共享的。如果修改了数据,不共享了。
- 读时共享(子进程被创建,两个进程没有做任何的写的操作),写时拷贝。

*/
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main() {

int num = 10;

// 创建子进程
pid_t pid = fork();

// 判断是父进程还是子进程
if(pid > 0) {
// printf("pid : %d\n", pid);
// 如果大于0,返回的是创建的子进程的进程号,当前是父进程
printf("i am parent process, pid : %d, ppid : %d\n", getpid(), getppid());

printf("parent num : %d\n", num);
num += 10;
printf("parent num += 10 : %d\n", num);

} else if(pid == 0) {
// 当前是子进程
printf("i am child process, pid : %d, ppid : %d\n", getpid(),getppid());

printf("child num : %d\n", num);
num += 100;
printf("child num += 100 : %d\n", num);
}

// for循环, 父子进程交替执行
for(int i = 0; i < 3; i++) {
printf("i : %d , pid : %d\n", i , getpid());
sleep(1);
}

return 0;
}

/*
实际上,更准确来说,Linux 的 fork() 使用是通过写时拷贝 (copy-on-write) 实现。
写时拷贝是一种可以推迟甚至避免拷贝数据的技术。
内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。
只用在需要写入的时候才会复制地址空间,从而使各个进行拥有各自的地址空间。
也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享。
注意:fork之后父子进程共享文件,
fork产生的子进程与父进程相同的文件文件描述符指向相同的文件表,引用计数增加,共享文件偏移指针。
*/

GDB 多进程调试

gdb默认只跟踪一个进程,默认调试父进程,子进程代码直接运行。设置调试默认调试的进程:

1
(gdb) set follow-fork-mode [parent (default) | child]

设置调试的模式:

1
(gdb) set detach-on-fork [on (default) | off]

on:调试时其他进程继续运行;

off:调试时其他进程被 gdb 挂起。

1
2
3
4
5
6
7
8
# 查看调试的进程
info inferiors

# 切换当前调试的进程
inferior id

# 使进程脱离 gdb 调试
detach inferiors id

exce 函数族

函数名称不同,功能相似的函数叫函数族。

  • exec 函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。
  • exec 函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程 ID 等一些表面上的信息仍保持原样。只有调用失败了,它们才会返回 -1,从原程序的调用点接着往下执行。
1
2
3
4
5
6
7
8
9
10
// 标准 C 库函数
int execl(const char *path, const char *arg, .../* (char *) NULL */);
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
int execle(const char *path, const char *arg, .../*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);

// linux 系统调用
int execve(const char *filename, char *const argv[], char *const envp[]);
  • l(list):参数地址列表,以空指针结尾
  • v(vector) :存有各参数地址的指针数组的地址
  • p(path) :按 PATH 环境变量指定的目录搜索可执行文件
  • e(environment) :存有环境变量字符串地址的指针数组的地址
  1. int execl(const char *path, const char *arg, .../* (char *) NULL */);
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/*  
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
- 参数:
- path:需要指定的执行的文件的路径或者名称
a.out /workspace/cpp_test/a.out 推荐使用绝对路径
./a.out hello world

- arg:是执行可执行文件所需要的参数列表
第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称
从第二个参数开始往后,就是程序执行所需要的的参数列表。
参数最后需要以NULL结束(哨兵)

- 返回值:
只有当调用失败,才会有返回值,返回-1,并且设置errno
如果调用成功,没有返回值。

*/
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>

int main() {

// 创建一个子进程,在子进程中执行exec函数族中的函数
pid_t pid = fork();

if(pid > 0) {
// 父进程
printf("i am parent process, pid : %d\n",getpid());
sleep(1);
}else if(pid == 0) {
// 子进程
execl("hello","hello",NULL);
// execl("/bin/ps", "ps", "aux", NULL);
perror("execl");
printf("i am child process, pid : %d\n", getpid()); // 不执行
}

for(int i = 0; i < 3; i++) {
printf("i = %d, pid = %d\n", i, getpid());
}

return 0;
}

/*
执行结果:
i am parent process, pid : 9175
hello, world
i = 0, pid = 9175
i = 1, pid = 9175
i = 2, pid = 9175
*/
  1. int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/*  
#include <unistd.h>
int execlp(const char *file, const char *arg, ... );
- 会到环境变量中查找指定的可执行文件,如果找到了就执行,找不到就执行不成功。
- 参数:
- file:需要执行的可执行文件的文件名
a.out
ps

- arg:是执行可执行文件所需要的参数列表
第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称
从第二个参数开始往后,就是程序执行所需要的的参数列表。
参数最后需要以NULL结束(哨兵)

- 返回值:
只有当调用失败,才会有返回值,返回-1,并且设置errno
如果调用成功,没有返回值。


int execv(const char *path, char *const argv[]);
argv是需要的参数的一个字符串数组
char * argv[] = {"ps", "aux", NULL};
execv("/bin/ps", argv);

*/
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>

int main() {

// 创建一个子进程,在子进程中执行exec函数族中的函数
pid_t pid = fork();

if(pid > 0) {
// 父进程
printf("i am parent process, pid : %d\n",getpid());
sleep(1);
}else if(pid == 0) {
// 子进程
execlp("ps", "ps", "j", NULL);

perror("execl");

printf("i am child process, pid : %d\n", getpid());
}

for(int i = 0; i < 3; i++) {
printf("i = %d, pid = %d\n", i, getpid());
}

return 0;
}

/*
执行结果:
i am parent process, pid : 11115
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
2578 2579 2579 2579 pts/0 11115 Ss 500 0:00 -bash
2579 11115 11115 2579 pts/0 11115 S+ 500 0:00 ./execlp.out
11115 11116 11115 2579 pts/0 11115 R+ 500 0:00 ps j
i = 0, pid = 11115
i = 1, pid = 11115
i = 2, pid = 11115
*/

进程控制

进程退出

1
2
3
4
5
6
7
// C 标准库
#include <stdlib.h>
void exit(int status);

// Liunx 系统调用
#include <unistd.h>
void _exit(int status);

进程退出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
#include <stdlib.h>
void exit(int status);

#include <unistd.h>
void _exit(int status);

status参数:是进程退出时的一个状态信息。父进程回收子进程资源的时候可以获取到。
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {

printf("hello\n");
printf("world");

exit(0); // 相当于 return 0;
// _exit(0);

return 0;
}

exit(0) 执行的结果(会刷新缓冲区):

exit(0)

_exit(0) 执行的结果(不会刷新缓冲区):

_exit(0)

孤儿进程

父进程运行结束,但子进程还在运行(未运行结束),这样的子进程就称为孤儿进程(Orphan
Process)。
每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为init,而 init 进程会循环地 wait() 它
的已经退出的子进程。

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

int main() {

// 创建子进程
pid_t pid = fork();

// 判断是父进程还是子进程
if(pid > 0) {

printf("i am parent process, pid : %d, ppid : %d\n", getpid(), getppid());

} else if(pid == 0) {
sleep(1);
// 当前是子进程
printf("i am child process, pid : %d, ppid : %d\n", getpid(),getppid());

}
return 0;
}

执行结果:

子进程的文件描述符表里的012复制于父进程,所以输出到同一个终端。

孤儿进程

僵尸进程

  • 每个进程结束之后, 都会释放自己地址空间中的用户区数据,内核区的 PCB 没有办法自己释放掉,需要父进程去释放。
  • 子进程终止时,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。
  • 僵尸进程不能被 kill -9 杀死,这样就会导致一个问题,如果父进程不调用 wait() 或 waitpid() 的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害,应当避免。

输出:

1
2
3
4
5
6
7
8
9
i am parent process, pid : 17195, ppid : 13038
i am child process, pid : 17196, ppid : 17195
i : 0 , pid : 17196
i : 1 , pid : 17196
i : 2 , pid : 17196
i am parent process, pid : 17195, ppid : 13038
i am parent process, pid : 17195, ppid : 13038
i am parent process, pid : 17195, ppid : 13038
...

新建终端使用 ps命令查看,子进程变为僵尸进程:

1
2
3
PPID   PID  PGID   SID  TTY      TPGID STAT   UID   TIME COMMAND
13038 17195 17195 13038 pts/0 17195 S+ 500 0:00 ./zombie
17195 17196 17195 13038 pts/0 17195 Z+ 500 0:00 [zombie] <defunct>

进程回收

在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。但是仍然为其保留一定的信息,这些信息主要主要指进程控制块PCB的信息(包括进程号、退出状态、运行时间等)。
父进程可以通过调用 wait 或 waitpid 得到它的退出状态同时彻底清除掉这个进程。
wait() 和 waitpid() 函数的功能一样,区别在于,wait() 函数会阻塞,waitpid() 可以设置不阻塞,
waitpid() 还可以指定等待哪个子进程结束。
注意:一次 wait 或 waitpid 调用只能清理一个子进程,清理多个子进程应使用循环。

wait() 函数

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/*
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
功能:等待任意一个子进程结束,如果任意一个子进程结束了,次函数会回收子进程的资源。
参数:int *wstatus
进程退出时的状态信息,传入的是一个int类型的地址,传出参数。
返回值:
- 成功:返回被回收的子进程的id
- 失败:-1 (所有的子进程都结束,调用函数失败)

调用wait函数的进程会被挂起(阻塞),直到它的一个子进程退出或者收到一个不能被忽略的信号时才被唤醒(相当于继续往下执行)
如果没有子进程了,函数立刻返回,返回-1;如果子进程都已经结束了,也会立即返回,返回-1.

*/
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>


int main() {

// 有一个父进程,创建5个子进程(兄弟)
pid_t pid;

// 创建5个子进程
for(int i = 0; i < 5; i++) {
pid = fork();
if(pid == 0) {
break;
}
}

if(pid > 0) {
// 父进程
while(1) {
printf("parent, pid = %d\n", getpid());

// int ret = wait(NULL);

int st;
int ret = wait(&st);
if(ret == -1) { //所有的子进程都结束
break;
}
if(WIFEXITED(st)) {
// 是不是正常退出
printf("退出的状态码:%d\n", WEXITSTATUS(st));
}
if(WIFSIGNALED(st)) {
// 是不是异常终止
printf("被哪个信号干掉了:%d\n", WTERMSIG(st));
}
printf("child die, pid = %d\n", ret);
sleep(1);
}

} else if (pid == 0){
// 子进程
// kill 信号
while(1) {
printf("child, pid = %d\n",getpid());
sleep(1);
}

// 正常退出
// printf("child, pid = %d\n",getpid());

exit(0);
}

return 0; // exit(0)
}

退出信息相关宏函数:

1
2
3
4
5
6
7
WIFEXITED(status)     // 非0,进程正常退出
WEXITSTATUS(status) // 如果上宏为真,获取进程退出的状态(exit的参数)
WIFSIGNALED(status) // 非0,进程异常终止
WTERMSIG(status) // 如果上宏为真,获取使进程终止的信号编号
WIFSTOPPED(status) // 非0,进程处于暂停状态
WSTOPSIG(status) // 如果上宏为真,获取使进程暂停的信号的编号
WIFCONTINUED(status) // 非0,进程暂停后已经继续运行

waitpid() 函数

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
/*
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *wstatus, int options);
功能:回收指定进程号的子进程,可以设置是否阻塞。
参数:
- pid:
pid > 0 : 某个子进程的pid
pid = 0 : 回收当前进程组的任一子进程
pid = -1 : 回收任一的子进程,相当于 wait() (最常用)
pid < -1 : 某个进程组的组id的绝对值,回收指定进程组中的子进程
- options:设置阻塞或者非阻塞
0 : 阻塞
WNOHANG : 非阻塞
- 返回值:
> 0 : 返回子进程的id
= 0 : options=WNOHANG, 表示还有子进程活着
= -1 :错误,或者没有子进程了


getpgrp函数可获取当前进程的进程组ID。
getpgid函数可获取指定进程的进程组ID。
setpgid函数可改变进程默认的进程组,可加入另一个进程组或创建进程组。
*/
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main() {

// 有一个父进程,创建5个子进程(兄弟)
pid_t pid;

// 创建5个子进程
for(int i = 0; i < 5; i++) {
pid = fork();
if(pid == 0) {
break;
}
}

if(pid > 0) {
// 父进程
while(1) {
printf("parent, pid = %d\n", getpid());
sleep(1);

int st;
// int ret = waitpid(-1, &st, 0); // 相当于 wait(&st);
int ret = waitpid(-1, &st, WNOHANG);

if(ret == -1) {
break;
} else if(ret == 0) {
// 说明还有子进程存在
continue;
} else if(ret > 0) {

if(WIFEXITED(st)) {
// 是不是正常退出
printf("退出的状态码:%d\n", WEXITSTATUS(st));
}
if(WIFSIGNALED(st)) {
// 是不是异常终止
printf("被哪个信号干掉了:%d\n", WTERMSIG(st));
}

printf("child die, pid = %d\n", ret);
}

}

} else if (pid == 0){
// 子进程
while(1) {
printf("child, pid = %d\n",getpid());
sleep(1);
}
exit(0);
}

return 0;
}

Linux 多进程开发(1)
https://ww1820.github.io/posts/b76a0a04/
作者
AWei
发布于
2022年8月15日
更新于
2022年8月15日
许可协议