本文最后更新于:2021年4月4日 下午
概览:Linux进程,多进程,fork、execve。
查看进程的指令
- ps aux
`
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
一次调用、两次返回,这是一个很特殊的函数。
实例:
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
|
从结果可以看出,
- 父子进程的运行是同时的,或早或晚,不可预见。
- 父子进程都拥有函数内的变量,但是互不干扰。
- 父进程的父亲的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, ...);
int execlp(const char *file, const char *arg, ... );
int execle(const char *path, const char *arg, ...);
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, ...);
|
实例:子进程调用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() {
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); 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, ... );
|
实例
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);
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);
|
案例:查看输出结果
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); 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);
|
案例:
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);
|
案例
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() {
pid_t pid;
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, 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; }
|