Linux操作系统———进程处理
什么是进程?
进程是正在运行的程序,是操作系统进行资源分配和调度的基本单位,程序是存储在硬盘或内存的一段二进制序列,是静态的,而进程是动态的,进程包括代码,数据以及分配给它的其他系统资源(如文件描述符,网络连接等)。
我们打开的VMWare、开启的浏览器都对应操作系统的一个进程。
在Linux中,进程是程序的一次执行实例,每个进程由内核通过PCB(Process Control Block) 管理,包含PID,状态,内存映射,文件描述符表等信息。
本篇我们来学习四个问题:
(1)如何启动一个新程序?
(2)如何在当前程序中并发执行多个任务?
(3)子进程结束后,父进程如何获知其退出状态?
(4)如果父进程先退出,子进程会怎样?
我们先来简单介绍几个函数的应用
——————fork函数的使用——————
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
//调用fork之前,代码都在父进程中运行,
//子进程复制父进程的地址空间(包括代码、数据、堆栈、文件描述符等)
printf("海哥教老学员%d春暖花开\n",getpid());
//使用fork创建子进程
/**
* 不需要传参,调用一次,返回两次
* return:int 进程号
* (1):-1出错
* (2):父进程中表示子进程的pid
* (3):子进程中显示为0
* __pid_t fork (void)
*/
pid_t pid = fork();
//从fork之后 所有的代码都是在父子进程中各执行一次的
if (pid < 0)
{
printf("新学员加入失败\n");
exit(EXIT_FAILURE);
}else if(pid == 0)
{
//执行单独子进程代码
printf("新学员%d加入成功,他是老学员%d推荐的\n",getpid(),getppid());
}else{
//执行单独父进程代码
printf("老学员%d继续深造,他推荐了%d\n",getpid(),pid);
}
return 0;
}
——————运行日志内容——————
海哥教老学员17042春暖花开
老学员17042继续深造,他推荐了17043
新学员17043加入成功,他是老学员17042推荐的
通过这个函数我们可以知道,可以使用fork函数来复制当前进程的地址空间(包括代码、数据、堆栈、文件描述符等)然后创建新的子进程,这个函数的返回值有三个,返回-1代表出错,返回零代表执行子进程的代码,返回当前进程代表执行父进程的代码
#include <sys/types.h>
#include <unistd.h>
getpid()这个用来获取当前进程PID
getppid()这个用来获取当前进程的父进程PID
————————fork函数与文件描述符fd的配合使用————————
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
int main(int argc, char const *argv[])
{
//调用fork之前
//打开一个文件
int fd = open("io.txt",O_CREAT | O_WRONLY | O_APPEND,0644);
if(fd == -1)
{
perror("open");
exit(EXIT_FAILURE);
}
char buffer[1024];//缓冲区存放读写的数据
pid_t pid = fork();
if (pid < 0)
{
printf("新学员加入失败\n");
exit(EXIT_FAILURE);
}else if(pid == 0)
{
//执行单独子进程代码
strcpy(buffer,"这是子进程写入的数据\n");
}else{
//执行单独父进程代码
sleep(1);
strcpy(buffer,"这是父进程写入的数据\n");
}
//父子进程都要执行的代码
ssize_t bytes_write = write(fd,buffer,strlen(buffer));
if(bytes_write == -1)
{
perror("write");
close(fd);
exit(EXIT_FAILURE);
}
printf("写入成功\n");
close(fd);
if(pid == 0)
{
printf("子进程写入完毕,并释放文件描述符\n");
}else{
printf("父进程写入完毕,并释放文件描述符\n");
}
return 0;
}
————————io.txt————————
这是子进程写入的数据
这是父进程写入的数据
————————日志运行内容————————
写入成功
子进程写入完毕,并释放文件描述符
写入成功
父进程写入完毕,并释放文件描述符
通过日志我们发现父进程和子进程都写入了数据,成功演示了父子进程共享文件描述符的示例,
也就是说父进程打开一个文件,然后fork()创建子进程,父子进程各子向同一个文件描述符写入不同内容,最后各自关闭该描述符。
open函数是在fork之前调用的,此时只有父进程存在,为什么运行的结果显示子进程也写进数据去了呢,因为在fork之后子进程会复制父进程的整个文件描述符表,也就是我们上个代码的结论,在fork之后子进程会复制父进程的地址空间(包括代码、数据、堆栈、文件描述符等),所以此时子进程的fd = 3也指向同一个内核打开文件表项,内核将该表项的引用计数+1变成了2,因为我使用了O_APPEND,所以父子进程都是分别写入的,不会被互相覆盖
- 子进程
close(fd)→ 引用计数 2 → 1,不释放内核资源; - 父进程
close(fd)→ 引用计数 1 → 0,内核释放该打开文件表项; - 此时文件才真正“关闭”。
也就是说刚开始的时候父进程创建三号指向这个结构体,然后他的文件描述符的引用计数是1,当fork创建一个子进程的时候,首先文件描述符的这个表他会原原本本的复制一份,复制完成之后,他就会说我这边也引用了这个对应的文件描述符,所以呢他会指向这个内核,在这个内核里面他会把文件描述符的引用计数加一,子进程调close的时候减一,因为父进程还在引用他,不为零就不会被释放,当父进程也close减一,为零了,他就会被释放调,也就被从内核当中删除掉了(图片来自b站up主尚硅谷,讲的非常好)
————————execve函数的使用————————
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
//跳转之前
char *name = "banzhang";
printf("我是%s,编号%d,我现在还在一楼\n",name,getpid());
//执行跳转
/**
* const char *__path:执行程序的路径
* char *const __argv[]:传入的参数-> 对应执行程序main方法的第二个参数
* (1):第一个参数固定是程序的名称->执行程序的路径
* (2):执行程序需要传入的参数
* (3):最后一个参数一定是NULL
* char *const __envp[]:传递的环境变量
* (1):环境变量参数:key = value
* (2):最后一个参数一定是NULL
* return:成功根本没办法返回,下面的代码也没有意义,失败返回-1
* 跳转前后只有进程号保留下来,别的变量都删除了
* int execve (const char *__path, char *const __argv[],char *const __envp[])
*/
char *args[] = {"/home/sxf/process_test/erlou",name,NULL};
char *envs[] = {"PATH=/home/sxf/bin:/usr/local/sbin:/usr/local/bin:/usr\
/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap\
/bin:/snap/bin",NULL};
int re = execve(args[0],args,envs);
if (re == -1)
{
printf("你没有机会上二楼\n");
exit(EXIT_FAILURE);
}
//此处代码没有意义,因为程序跳转了,不会再往下执行了
return 0;
}
————————erlou.c————————
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
if(argc < 2)
{
printf("参数不够不能上二楼\n");
return 1;
}
printf("我是%s,编号%d,我跟海哥上二楼啦\n",argv[1],getpid());
return 0;
}
————————运行日志的内容————————
我是banzhang,编号17122,我现在还在一楼
我是banzhang,编号17122,我跟海哥上二楼啦
通过程序我们看到,这个函数的用法是从当前程序跳转到另一个程序erlou.c
这里我们特别介绍一下这个函数的参数,第一个参数是要执行的程序的路径,在这里我传的是绝对路径,第二个参数是传给新程序的参数数组,第三个参数是环境变量,如果你第一个参数给的是绝对路径的话,其实这个环境变量数组可以只填NULL,如何获取PATH路径呢,你只需要在你终端输入$PATH输出的结果就是了
接下来让我们将fork函数和execve函数配合使用
————————fork和execve配合使用——————
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
//邀请之前
char * name = "老学员";
printf("%s %d继续在一楼精进\n",name,getpid());
//邀请新学员
__pid_t pid = fork();
if(pid == -1)
{
printf("邀请新学员失败\n");
exit(EXIT_FAILURE);
}else if(pid == 0)
{
//新学员在这
char *new_name = "ergou";
char *args[] = {"/home/sxf/process_test/erlou",new_name,NULL};
char *envs[] = {NULL};
int exR = execve(args[0],args,envs);
if(exR == -1)
{
printf("新学员上二楼失败\n");
exit(EXIT_FAILURE);
}
//新学员上二楼成功 此处代码不执行
}else
{
//老学员在这
printf("老学员%d,邀请完新学员%d之后,还是在一楼学习\n",getpid(),pid);
}
printf("11111");
return 0;
}
————————erlou.c————————
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
if(argc < 2)
{
printf("参数不够不能上二楼\n");
return 1;
}
printf("我是%s,编号%d,我跟海哥上二楼啦\n",argv[1],getpid());
return 0;
}
————————运行日志内容————————
老学员 17464继续在一楼精进
老学员17464,邀请完新学员17465之后,还是在一楼学习
11111
我是ergou,编号17465,我跟海哥上二楼啦
我们根据这个日志的运行内容可以看到,我们调用了fork函数创建了一个新的子进程,然后在这个新的子进程中呢又调用execve函数跳转到另一个程序erlopu.c中,这就实现了父进程创建出一个子进程,然后子进程去运行另一个程序,父进程自己保持不变。
——————system函数的使用——————
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
/**
* 使用标准库函数创建子进程
* const char *__command:使用Linux命令直接创建一个子进程
* return:成功返回0,失败返回编号
* int system (const char *__command)
*/
int sysR = system("ping -c 100 www.baidu.com");
if (sysR != 0)
{
perror("system");
exit(EXIT_FAILURE);
}
return 0;
}
system这个函数可以用在程序中调用外部命令,他的参数是一个以NULL结尾的字符串,表示要执行的shell命令,这个函数启动了一个 shell(通常是 /bin/sh),让 shell 去解释并执行传入的命令字符串。
Linux中父进程除了可以启动子进程,还要负责回收子进程的状态,如果子进程结束后父进程没有正常回收,那么子进程就会变成一个僵尸进程------即程序执行完成,但是进程没有完全结束,其内核中PCB结构体没有释放。
————————waitpid函数的使用————————
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc, char const *argv[])
{
//fork之前
int subprocrss_status;
printf("老学员在校区\n");
__pid_t pid = fork();
if (pid < 0)
{
perror("fork");
return 1;
} else if(pid == 0)
{
//新学员
char *args[] = {"/usr/bin/ping","-c","3","www.baidu.com",NULL};
char *envs[] = {"PATH=/home/sxf/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/snap/bin",NULL};
printf("新学员%d联系海哥20次\n",getpid());
int exR = execve(args[0],args,envs);
if (exR < 0)
{
perror("execve");
return 1;
}
}else {
//老学员
printf("老学员%d等待新学员%d联系\n",getpid(),pid);
waitpid(pid,&subprocrss_status,0);
}
printf("老学员等待新学员联系完成\n");
return 0;
}
————————日志运行内容————————
老学员在校区
老学员18041等待新学员18042联系
新学员18042联系海哥20次
PING www.a.shifen.com (39.156.70.46) 56(84) bytes of data.
64 bytes from 39.156.70.46 (39.156.70.46): icmp_seq=1 ttl=128 time=41.6 ms
64 bytes from 39.156.70.46 (39.156.70.46): icmp_seq=2 ttl=128 time=74.1 ms
64 bytes from 39.156.70.46 (39.156.70.46): icmp_seq=3 ttl=128 time=53.7 ms
--- www.a.shifen.com ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 41.599/56.449/74.059/13.394 ms
老学员等待新学员联系完成
我们来看一下waitpid这个函数的参数:
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
第一个参数是指定等待哪个子进程:
|
|
含义 |
|---|---|
|
|
等待指定 PID 的子进程(最常用)<br>例: |
|
|
等待任意子进程(等价于 |
|
|
等待同进程组的任意子进程 |
|
|
等待进程组 ID = |pid| 的任意子进程 |
第二个参数是接收子进程退出状态,如果不关心状态,可以传NULL
第三个参数是控制等待行为:
|
选项 |
说明 |
|---|---|
|
|
阻塞等待:直到指定子进程结束才返回(最常用) |
|
|
非阻塞:若子进程未结束,立即返回 |
|
|
也报告暂停(stopped) 的子进程(如被 |
|
|
报告从暂停恢复的子进程(需 |
用waitpid等待子进程完成再去接着运行父进程就能有效避免僵尸进程的产生。
最后在总结一下僵尸进程和孤儿进程的区别:
|
特性 |
僵尸进程(Zombie Process) |
孤儿进程(Orphan Process) |
|---|---|---|
|
定义 |
子进程已终止(exit),但父进程尚未调用 |
父进程先于子进程退出,子进程仍在运行 |
|
进程状态 |
已结束,不占用 CPU/内存,但内核 PCB 未释放 |
仍在正常运行,占用系统资源 |
|
存在位置 |
进程表中保留(PID 仍被占用) |
进程表中正常存在,可执行代码 |
|
危害 |
占用 PID 资源(大量僵尸可耗尽 PID 空间) |
无危害(会被 init/systemd 收养) |
|
如何产生 |
父进程 fork 子进程后,子进程 exit,但父进程不 wait |
父进程先 exit,子进程还在运行 |
|
如何解决 |
父进程必须调用 |
无需手动处理,系统自动收养 |
|
|
状态为 |
状态为 |
|
生命周期 |
直到父进程 wait 或父进程退出(此时转为孤儿,被 init wait) |
被 init(PID=1)或 systemd 收养,成为其子进程 |
更多推荐

所有评论(0)