Linux系统编程---多进程
一、进程的概念
1、什么是程序?什么是进程?
- 程序:编译后产生的,格式为ELF的,存储于硬盘的文件。
- 进程:程序中的代码和数据,被加载到内存中运行的过程。其实说白了,进程就是一个正在执行的程序。
- 程序是静态的概念,进程是动态的概念。
在Linux中,程序文件的格式都是ELF,这些文件在被执行的瞬间,就被载入内存,所谓的载入内存,如上图所示,就是将数据段、代码段这些运行时必要的资源拷贝到内存,另外系统会再分配相应的栈、堆等内存空间给这个进程,使之成为一个动态的实体。
2、为什么需要多进程?
多进程(Multiprocessing)是操作系统提供的一种并发执行机制,允许系统同时运行多个进程。它的主要目的是提高计算机资源的利用率、增强程序的并发能力,并改善系统的整体性能。
单进程程序 -> 只能一行行代码去执行。
多进程程序 -> 同时执行两行及以上的代码 -> 产生一个或多个子进程,帮自己处理另其它的事情。
3、在linux下,如何开启一个新的进程?
直接在linux下,执行一个程序,就会开启相应的进程。
例如:在Linux的终端执行 ./hello -> 开启一个名字为hello的进程。
4、当进程开启之后,系统会为进程分配什么资源?
1)会分配进程对应内存空间
如int x -> 运行程序之后,就会在栈区申请4个字节的内存空间。
2)进程在系统内核中如何进行表示呢
学生管理系统 ---> 每个学生使用结构体进行表示和管理
linux系统 ---> 每个进程使用结构体进行表示和管理
当进程开启之后,会为这个进程分配一个任务结构体,这个任务结构体就是用于描述这个进程的。也就是说,进程在内核中是以结构体struct task_struct{} 进行表示的。这个结构体也被称之为进程控制块。
结构体:进程ID号、信号、文件、资源....
/usr/src/linux-headers-5.15.0-136/include/linux ----第717行
(不同版本系统的Linux可能存放的路径不同)
5、关于进程的命令
1)pstree:查看整个Linux系统进程之间关系的命令
在Linux系统中,除了系统的初始进程之外,其余所有进程都是通过从一个父进程(parent)复刻(fork)而来的,有点像人类社会,每个个体都是由亲代父母繁衍而来。
因此,在Linux系统中,所有的进程都起源于相同的初始进程,它们之间形成一棵倒置的进程树,就像家族族谱,可以使用命令pstree查看这些进程的关系:
Snail@ubuntu:~/Desktop$ pstree
systemd─┬─ModemManager───2*[{ModemManager}]├─NetworkManager───2*[{NetworkManager}]├─VGAuthService├─accounts-daemon───2*[{accounts-daemon}]├─acpid├─avahi-daemon───avahi-daemon├─blkmapd├─colord───2*[{colord}]├─cron├─cups-browsed───2*[{cups-browsed}]├─cupsd───dbus├─dbus-daemon├─freshclam├─fwupd───4*[{fwupd}]...
2)ps -ef:查看进程ID号(静态)
如下图所示,从最开始的系统进程叫systemd(init),这个进程的诞生比较特别,其身份信息在系统启动前就已经存在于系统分区之中,在系统启动时直接复制到内存。而其余的进程,从前面提到的pstree命令的执行效果可见,都是系统初始进程的直接或间接的后代进程。
3)top:查看进程CPU的占用率(动态)
Snail@ubuntu:~/Desktop$ toptop - 18:19:03 up 31 min, 1 user, load average: 0.10, 0.07, 0.14
Tasks: 342 total, 1 running, 341 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.7 us, 1.4 sy, 0.0 ni, 97.9 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 7894.6 total, 4216.9 free, 1381.5 used, 2296.2 buff/cache
MiB Swap: 1162.4 total, 1162.4 free, 0.0 used. 6187.0 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 1935 Snail 20 0 4665680 273728 130240 S 8.6 3.4 0:43.43 gnome-s+ 6117 Snail 20 0 556976 52472 39952 S 3.3 0.6 0:00.97 gnome-t+ 2230 Snail 20 0 498040 32856 24280 S 0.7 0.4 0:02.74 fcitx 2507 Snail 20 0 8484 3472 3200 S 0.7 0.0 0:01.95 dbus-da+ 17 root 20 0 0 0 0 I 0.3 0.0 0:18.27 rcu_pre+ 55 root 20 0 0 0 0 I 0.3 0.0 0:02.04 kworker+ ...
二、进程的状态
1、进程从诞生到死亡会经历哪些状态?
就绪态 TASK_RUNNING 等待CPU资源 不占用CPU资源,不运行代码。
运行态 TASK_RUNNING 占用CPU资源,运行代码。
暂停态 TASK_STOPPED 占用CPU资源,不运行代码。 可以到就绪态。
睡眠态 占用CPU资源,运行代码。 可以到就绪态。
TASK_INTERRUPTIBLE 响应信号 ----》浅度睡眠 pause()--->一直等待下一信号
TASK_UNINTERRUPTIBLE 不响应信号 ----》深度睡眠
僵尸态 EXIT_ZOMBIE 占用CPU资源,不运行代码。不可以到运行态。进程退出的时候,就一定会变成僵尸态。
死亡态 EXIT_DEAD 不占用CPU资源,不运行代码。进程退出的时候,如果有人去帮自己回收资源,那么僵尸态就会变为死亡态。
2、什么是僵尸态?
进程结束时,就从运行态变成僵尸态,所谓僵尸态,就是代表这个进程所占用的CPU资源和自身的任务结构体没有被释放,这个状态的进程就是僵尸态进程。
总结:
1)进程在暂停态时,收到继续的信号时,是切换到就绪态,而不是运行态。
2)程序的main函数执行return 0就会导致进程的退出,一定会变成僵尸态。
3)进程不可以没有父进程,也不能同时拥有两个父进程。
4)孤儿进程特征就是失去父进程时,会马上寻找继父,而不是等到孤儿进程变成僵尸态再找。
5)祖先进程一定要帮其他的进程回收资源。
三、进程创建
1、fork:在一个正在运行的进程中创建一个子进程
#include <sys/types.h>
#include <unistd.h>函数原型:pid_t fork(void);
函数作用:创建一个新的进程(子进程),该进程是调用进程(父进程)的副本。子进程会复制父进程的:代码段(text segment)数据段(data segment)、堆(heap)、栈(stack)文件描述符表(但共享相同的文件表项)进程环境(环境变量、信号处理方式等)写时复制(Copy-On-Write, COW)优化:现代操作系统不会立即复制所有内存,而是共享父进程的内存页。只有当父进程或子进程尝试修改某块内存时,才会真正复制该内存页,以提高效率。
返回值:成功时(fork() 调用一次,但返回两次):在父进程中:返回子进程的 PID(> 0)在子进程中:返回 0(因为子进程可以通过 getppid() 获取父进程的 PID)
失败时:返回-1,并设置 errno(常见原因:进程数达到系统限制、内存不足等)。
在进程内部创建一个新的子进程,看看会不会同时做两件事情。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>int main()
{/*当前是单进程的程序*/ printf("main process!\n");/*产生一个子进程*/fork();/* 一个父进程,一个子进程 *//* 接下来的代码,父进程会执行一遍,子进程也会执行一遍 */printf("after fork\n");return 0;
}
可能的运行结果分析:
结果1:父进程先运行,子进程后运行。
注意:只有父进程退出,才会出现命令行,子进程退出是不会出现命令行。
结果2:子进程先运行,父进程后运行。
想要确保每次都是子进程先运行,做法: 就是让父进程先睡眠。
子进程不用睡眠,父进程需要睡眠。 父子进程任务不一样。
思路: 通过返回值判断。
#include<stdio.h>
#include <sys/types.h>
#include <unistd.h>int main()
{printf("main....\n");pid_t id = fork(); //创建一个子进程if(id == -1) //进程创建失败{perror("fork process error");return -1;}else if(id > 0) //父进程 ,但是返回的ID号 是 子进程的ID号{sleep(1); printf("我是父进程,我的进程ID是:%d\n", getpid());printf("我儿子的进程ID是:%d\n",id);}else if(id == 0)//子进程{printf("我是儿子,我的进程ID是:%d\n", getpid()); }printf("hello\n");return 0;
}
结论:
1)父子进程执行顺序随机的。
2)fork()之后的代码,两个进程都会执行。
四、查看进程号
1、getpid
查看当前进程号
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>函数原型:pid_t getpid(void);函数功能:返回当前进程的进程 ID(PID)。每个进程都有一个唯一的 PID,由内核分配。返回值:成功时返回当前进程的 PID(> 0)。不会失败(没有错误情况)。
2、getppid
查看父进程号
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>函数原型:pid_t getppid(void);函数功能:返回当前进程的父进程 ID(PPID)。如果父进程终止,子进程的 PPID 会变为1(init 或 systemd),这种情况称为“孤儿进程被 init 收养”。返回值:成功时返回当前进程的 PPID(> 0)。不会失败(没有错误情况)。
3、程序实例
#include <stdio.h>
#include <unistd.h>int main() {pid_t pid = fork();if (pid == -1) {perror("fork failed");return 1;} else if (pid == 0) {// 子进程printf("Child: PID = %d, PPID = %d\n", getpid(), getppid());} else {// 父进程printf("Parent: PID = %d, child PID = %d\n", getpid(), pid);}return 0;
}
五、进程回收
1、wait
#include <sys/types.h>
#include <sys/wait.h>函数原型:pid_t wait(int *wstatus);
函数功能:阻塞当前进程;等待其子进程退出并回收其系统资源;把子进程从僵尸状态设置为死亡状态。
函数参数:wstatus --> 用于存储子进程的状态信息 , 设置为NULL 则表示不需要状态信息
返回值:成功 返回接收到的子进程的ID 失败 返回 -1 ,并设置错误码
接口说明:
- 如果当前进程没有子进程,则该函数立即返回。
- 如果当前进程有不止1个子进程,则该函数会回收第一个变成僵尸态的子进程的系统资源。
- 子进程的退出状态(包括退出值、终止信号等)将被放入wstatus所指示的内存中,若wstatus指针为NULL,则代表当前进程放弃其子进程的退出状态。
- 如果父进程如果要获取这些信息,需要用以下宏对status进程解析:
宏 | 功能 |
WIFEXITED(status) | 判断子进程是否正常退出 |
WEXITSTATUS(status) | 获取正常退出的子进程的退出值 |
WIFSIGNALED(status) | 判断子进程是否被信号杀死 |
WTERMSIG(status) | 获取杀死子进程的信号的值 |
2、waitpid
#include <sys/types.h>
#include <sys/wait.h>函数原型:
pid_t waitpid(pid_t pid, int *wstatus, int options);函数功能:
挂起(阻塞)当前进程,直到指定的子进程终止或收到信号。
回收子进程资源(防止僵尸进程)。
获取子进程退出状态(通过 wstatus 返回)。函数参数:pid> 0 等待指定pid的子进程(假设为138 那么表示等待138号进程退出)= 0 等待本进程组中的任意一个子进程的退出 (与wait功能一致)= -1 等待任意一个子进程 (与进程组无关)< -1 等待的是该进程组(假设 pid 为 -138 那么表示等待进程组ID为 138的任何一个进程)wstatus --> 存储退出状态options --> options的取值,可以是0,也可以是上表中各个不同的宏的位或运算取值。
返回值:> 0 成功返回终止的子进程 PID。= 0 仅当options=WNOHANG时,表示没有子进程退出。-1 出错(如无子进程、被信号中断等),错误码存于errno。
与wait()的区别:
- 可以通过参数 pid 用来指定想要回收的子进程。
- 可以通过 options 来指定非阻塞等待。
pid | 作用 | options | 作用 |
等待组ID等于pid绝对值的进程组中的任意一个子进程 | 0 | 阻塞等待子进程的退出 | |
-1 | 等待任意一个子进程 | WNOHANG | 若没有僵尸子进程,则函数立即返回(非阻塞) |
0 | 等待本进程所在的进程组中的任意一个子进程 | WUNTRACED | 当子进程暂停时函数返回(除了退出以外子进程暂停也会结束阻塞) |
>0 | 等待指定pid的子进程 | WCONTINUED | 当子进程收到信号SIGCONT继续运行时函数返回(当子进程收到继续运行是会结束阻塞) |
六、进程退出
1、exit()
exit() 是 C 标准库(stdlib.h)提供的进程终止函数,用于 正常结束程序 并返回状态码给操作系统。它比 _exit() 更安全,因为它会先执行清理工作(如刷新缓冲区)。
#include <stdlib.h>函数原型:void exit(int status);函数作用:先清洗缓冲区和文件指针,再退出参数:status: 退出状态值0 -> 进程正常退出非0 -> 进程异常退出返回值:无但进程的退出状态会通过系统调用 _exit(status) 传递给父进程(可通过waitpid() 获取)
2、_exit()
_exit() 是一个系统级的进程终止函数,定义在中unistd.h。它直接终止进程,不执行任何清理操作(如缓冲区刷新、atexit() 函数调用等),而是立即将控制权交还给操作系统。
#include <unistd.h>函数原型:
void _exit(int status);函数功能:不会清洗缓冲区、不会关闭文件描述符,直接终止进程函数参数:status: 退出状态值0 -> 进程正常退出非0 -> 进程异常退出返回值:无返回值进程的退出状态status 会传递给父进程(可通过 waitpid() 或 wait() 获取)。
3、_Exit()
_Exit() 是C 标准库(stdlib.h)提供的立即终止进程的函数,与 _exit() 类似,但属于 C 标准(而非 POSIX)。它 不执行任何清理操作,直接终止进程。
#include <stdlib.h>函数原型:
void _Exit(int status);函数功能:立即终止当前进程,不执行任何清理操作直接返回状态码 status 给操作系统(父进程可通过 waitpid() 获取)函数参数:status: 退出状态值0 -> 进程正常退出非0 -> 进程异常退出返回值:无返回值进程的退出状态status 会传递给父进程(通过 waitpid() 获取)。
4、 缓冲区问题
#include<stdio.h>
#include<stdbool.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>int main()
{/*单进程的程序*/printf("main ");//先清洗缓冲区,再退出当前进程//exit(0);//return 0;//直接退出当前进程,不会帮助你清洗缓冲区_exit(0);printf("11112222\n");
}
5、 退出状态
父进程监听子进程的退出状态,如果子进程正常退出的,则输出一个ok,如果子进程异常退出,则输出一个error。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc,char *argv[]) //./show_bmp
{pid_t x;int state; //买一个碗int a = 11;x = fork();if(x > 0){wait(&state); //将碗的地址给你if(state == 0){printf("ok!\n");}else{printf("error!\n");}}if(x == 0){sleep(5);if(a == 10){exit(0); //正常退出}else{exit(-1); //异常退出}}
}
6、 exit与return
例1:
#include <stdio.h>
#include <stdlib.h>int main(int argc,char *argv[])
{printf("helloworld!\n");return 0;
}
//输出helloworld
例2:
#include <stdio.h>
#include <stdlib.h>int main(int argc,char *argv[])
{printf("helloworld!\n");exit(0);
}
//输出helloworld
例3:
#include <stdio.h>
#include <stdlib.h>int fun(void)
{return 0;
}int main(int argc,char *argv[])
{fun();printf("helloworld!\n");return 0;
}
//输出helloworld
例4:
#include <stdio.h>
#include <stdlib.h>int fun(void)
{exit(0); -> 整个进程马上结束
}int main(int argc,char *argv[])
{fun();printf("helloworld!\n");return 0;
}
//不会输出helloworld
结论:
1)在main函数中,exit()与return语句作用是一样。 -> 因为main函数的返回就等价于进程的退出。
2)不在main函数中,exit()代表进程的退出,return只是代表函数的返回。