Linux 进程基础知识

本文最后更新于:2021年4月4日 下午

概览:Linux进程,多进程,fork、execve。

查看进程的指令

  1. ps aux

`

  • top

fork

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

pid_t fork(void);

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

失败的两个主要原因:
1. 当前系统的进程数已经达到了系统规定的上限,这时 errno 的值被设置为 EAGAIN
2. 系统内存不足,这时 errno 的值被设置为 ENOMEM
*/

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
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main(){

int num = 0;

pid_t pid = fork();

if(pid > 0){
//父进程
printf("parent process, pid : %d , ppid : %d \n",getpid(),getppid());

printf("parent num1 : %d\n",num);
num+=10;
printf("parent num2 : %d\n",num);
}
else if(pid == 0){
//子进程
printf("child process, pid : %d , ppid : %d \n",getpid(),getppid());


printf("child num1 : %d\n",num);
num+=20;
printf("child num2 : %d\n",num);

}

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

return 0;
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
parent process, pid : 6822 , ppid : 4568 
child process, pid : 6823 , ppid : 6822
parent num1 : 0
child num1 : 0
parent num2 : 10
child num2 : 20
nums : 0 , pid : 6822
nums : 0 , pid : 6823
nums : 1 , pid : 6822
nums : 1 , pid : 6823
nums : 2 , pid : 6823
nums : 2 , pid : 6822

从结果可以看出,

  1. 父子进程的运行是同时的,或早或晚,不可预见。
  2. 父子进程都拥有函数内的变量,但是互不干扰。
  3. 父进程的父亲的pid经过查询可以发现就是当前中终端,即终端也是一个进程。

父子进程的关系

父子进程之间的关系:

区别:

1.fork()函数的返回值不同

  • 父进程中: >0 返回的子进程的ID

  • 子进程中: =0

2.pcb中的一些数据

  • 当前的进程的id pid

  • 当前的进程的父进程的id ppid

  • 信号集

共同点:

某些状态下:子进程刚被创建出来,还没有执行任何的写数据的操作

  • 用户区的数据

  • 文件描述符表,共享

写时复制技术

调用fork,子进程拷贝父进程的内存中的全部内容,包括数据段、代码段等,仅仅内核区中的一些进程相关信息不同。

linux采用了写时复制的技术,即并非子进程一创建它就完全复制拷贝父进程的代码到其他内存位置,两者先是通过虚拟内存来指向同一段物理内存,当其需要写入、修改数据的时候,才会拷贝。

fork之后,操作系统将父进程内存区域指定为只读,此时父子进程都可以读取数据,但是一旦写操作,就会出现错误,会触发页面保护故障,然后就会激活写时复制,在实际的物理内存中复制,然后重新执行那段指令。


exec函数族

exec 函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件

exec 函数族的函数执行成功不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程 ID 等一些表面上的信息仍保持原样,只有调用失败了,它们才会返回 -1,从原程序的调用点接着往下执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//标准头文件的函数,实际上封装了系统函数
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[]);

//系统函数
int execve(const char *filename, char *const argv[], char *const envp[]);
1
2
3
4
l(list) 参数地址列表,以空指针结尾
v(vector) 存有各参数地址的指针数组的地址
p(path) 按 PATH 环境变量指定的目录搜索可执行文件
e(environment) 存有环境变量字符串地址的指针数组的地址

execl函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
/*
- 参数:
- path:需要指定的执行的文件的路径或者名称
a.out /home/nowcoder/a.out 推荐使用绝对路径
./a.out hello world

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

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

*/

实例:子进程调用ps aux

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <unistd.h>
#include <stdio.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("/bin/ps", "ps", "aux", NULL);//"ps"必须写,他是程序名字,然后最后以NULL结尾

//若上述代码执行失败,就会执行接下来的代码
perror("execl");
printf("i am child process, pid : %d\n", getpid());

}
return 0;
}

execlp函数

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
#include <unistd.h>
int execlp(const char *file, const char *arg, ... );
/*
- 会到环境变量中查找指定的可执行文件,如果找到了就执行,找不到就执行不成功。
- 参数:
- file:需要执行的可执行文件的文件名

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

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


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

int execve(const char *filename, char *const argv[], char *const envp[]);
char * envp[] = {"/home/nowcoder", "/home/bbb", "/home/aaa"};

*/

实例

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

int main() {
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", "aux", NULL);//注意和execl的区别

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

}
return 0;
}

进程退出

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

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

//status参数:是进程退出时的一个状态信息。父进程回收子进程资源的时候可以获取到。

案例:查看输出结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {

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

// exit(0);
_exit(0);

return 0;
}

两者区别:

输出结果:

1
2
3
4
5
6
7
exit(0)结果: world之后无换行,直接就是用户名
hello
worldcolourso@colourso-virtual-machine:~/learn/process$

_exit(0)结果:没有world,即此函数不会刷新IO缓冲
hello
colourso@colourso-virtual-machine:~/learn/process$

孤儿进程

父进程运行结束,但子进程还在运行(未运行结束),这样的子进程就称为孤儿进程(Orphan Process)。

每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为 init ,而 init 进程会循环地 wait() 它的已经退出的子进程。这样init进程会帮其回收资源。

因此孤儿进程并不会有什么危害。

案例:查看父进程id,函数getppid()

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>
#include <stdlib.h>

int main(){

pid_t pid = fork();

if(pid == 0){
//子进程
for(int i=0;i<3;i++){
printf("child, pid : %d ,ppid : %d\n",getpid(),getppid());
sleep(1);
}
}else if(pid >0){
//父进程
printf("parent,pid : %d ,ppid : %d \n",getpid(),getppid());
exit(0);
}
return 0;
}

最终结果:

1
2
3
4
5
colourso@colourso-virtual-machine:~/learn/process$ ./showinit 
parent,pid : 7201 ,ppid : 5854
child, pid : 7202 ,ppid : 7201
colourso@colourso-virtual-machine:~/learn/process$ child, pid : 7202 ,ppid : 1
child, pid : 7202 ,ppid : 1

可以看到,父进程终止之后,子进程的父进程pid变成了1,也就是init的pid。

僵尸进程

每个进程结束之后, 都会释放自己地址空间中的用户区数据,内核区的 PCB 没有办法自己释放掉,需要父进程去释放

进程终止时,父进程尚未回收子进程残留资源(PCB)存放于内核中,变成僵尸 (Zombie)进程。

僵尸进程不能被 kill -9 杀死,这样就会导致一个问题,如果父进程不调用 wait() 或 waitpid() 的话,那么保留的那段信息就不会释放,其进程号就会一直被占用, 但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害,应当避免。

案例:查看僵尸进程

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

int main(){

pid_t pid = fork();

if(pid >0){
//父进程
while(1){
printf("parent,pid :%d,ppid : %d \n",getpid(),getppid());
}
}
else if(pid == 0){
//子进程
printf("child ,pid :%d,ppid : %d \n",getpid(),getppid());
}
return 0;
}

开启另一个终端,查看ps aux的结果,以及符号Z+<defunct>就表示这是一个僵尸进程

1
2
colourso   8804 21.3  0.0   2480   836 pts/4    S+   14:07   0:02 ./zombie
colourso 8805 0.0 0.0 0 0 pts/4 Z+ 14:07 0:00 [zombie] <defunct>

进程回收

在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。但是仍然为其保留一定的信息,这些信息主要主要指进程控制块PCB的信息(包括进程号、退出状态、运行时间等)。

父进程可以通过调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。

wait() 和 waitpid() 函数的功能一样,区别在于,wait() 函数会阻塞,waitpid() 可以设置不阻塞,waitpid() 还可以指定等待哪个子进程结束。

注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。

退出信息相关的宏函数

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,进程暂停后已经继续运行

wait函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);

/*
功能:等待任意一个子进程结束,如果任意一个子进程结束了,次函数会回收子进程的资源。
参数:int *wstatus
进程退出时的状态信息,传入的是一个int类型的地址,传出参数。
返回值:
- 成功:返回被回收的子进程的id
- 失败:-1 (所有的子进程都结束,调用函数失败)

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

案例:

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
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>


int main() {

//创建五个进程
pid_t pid;

for(int i=0;i<5;i++){
pid = fork();

if(pid == 0){
break;//子进程不必执行
}
}

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

int st;
while(1){
int ret = wait(&st);

if(ret == -1){
break;//结束
}
else {
if(WIFEXITED(st)){
printf("退出的状态码:%d\n", WEXITSTATUS(st));
}

if(WIFSIGNALED(st)){
printf("被哪个信号干掉了:%d\n", WTERMSIG(st));
}

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

return 0;
}

开启另外一个终端,使用kill -9 pid的方式杀死子进程,可看到父进程的输出结果。

waitpid函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#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 :错误,或者没有子进程了
*/

案例

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
#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);
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;
}