目录
3.2.3 在命令行执行./a.out,程序是如何运行起来的
6.2.3 从进程终止状态中提取进程终止的原因、返回值或者信号编号
1. 有关进程
1.1 什么是进程
1.2进程ID(PID)
OS为了能够更好地管理进程,为每个进程分配了一个唯一的编号(非负整数),这个编号就是PID
- 如果当前进程结束了,这个PID可以被可以被重复使用,但是所有“活着”的进程,它们的进程ID一定都是唯一的。
- 因为ID唯一性,当我们想创建一个名字唯一的文件时,往往可以在文件名中加入PID,这样就能保证文件名唯一性。
1.3 三个特殊的进程
0、1、2这个三个进程,是OS启动起来后会一直默默运行的进程,直到关机OS结束运行
进程 ID == 0 的进程
进程ID == 1的进程
进程ID == 2的进程
1.4获取与进程相关的各种ID的函数
#include <sys/types.h> #include <unistd.h> pid_t getpid(void); pid_t getppid(void); uid_t getuid(void); gid_t getgid(void);
2. 程序的运行过程
2.1 程序如何运行起来
(1)在内存中划出一片内存空间
(2)将硬盘上可执行文件中的代码(机器指令)拷贝到划出的内存空间中
(3)pc指向第一条指令,cpu取指运行在Linux下,OS提供两个非常关键的API,一个是fork,另一个是exec。
fork :开辟出一块内存空间
exec:将程序代码(机器指令)拷贝到开辟的内存空间中,并让pc指向第一条指令,CPU开始执行,进程就运行起来了
运行起来的进程会与其它的进程切换着并发运行。
2.2 fork
2.2.1 函数原型
#include <unistd.h> pid_t fork(void);
2.2.2复制的原理
Linux有虚拟内存机制,所以父进程是运行在虚拟内存上的,虚拟内存是OS通过数据结构基于物理内存模拟出来的,因此底层的对应的还是物理内存
复制时子进程时,会复制父进程的虚拟内存数据结构,那么就得到了子进程的虚拟内存,相应的底层会对应着一片新的物理内存空间,里面放了与父进程一模一样代码和数据,
2.2.3 父子进程各自会执行哪些代码
![]()
验证子进程复制了父进程的代码和数据
printf("befor fork"); 在父子进程中都输出 因为没加换行符 所以 在子进程中遇见换行缓冲区被刷新到屏幕
2.3 父子进程共享操作文件
(1)情况1:独立打开文件
独立打开同一文件时,父子进程各自的文件描述符,指向的是不同的文件表。
因为拥有不同的文件表,所以他们拥有各自独立的文件读写位置,会出现相互覆盖情况
如果不想相互覆盖需要加O_APPEND标志。
(2)情况2:fork之前打开文件
子进程会继承父进程已经打开的文件描述符,
如果父进程的3描述符指向了某个文件,子进程所继承的文件描述符3也会指向这个文件。
由于共享的是相同的文件表,所以拥有共同的文件读写位置,不会出现覆盖的情况。
子进程的0 1 2这三个打开的文件描述符,其实也是从父进程那里继承过来的,并不是子进程自己去打开的
同样的父进程的 1 2又是从它的父进程那里继承过来的,最根溯源的话,都是从最原始的进程哪里继承过来的
2.4 子进程会继承父进程的哪些属性
2.4.1 子进程继承如下性质
- (1)用户ID,用户组ID
- (2)进程组ID
- (3)会话期ID
- (4)控制终端
- (5)当前工作目录
- (6)根目录
- (7)文件创建方式屏蔽字
- (8)环境变量
- (9)打开的文件描述符
- 等等
2.4.2 子进程独立的属性
- (1)进程ID。
- (2)不同的父进程ID。
- (3)父进程设置的锁,子进程不能被继承。
- 等等
3. exec加载器
3.1 exec的作用
父进程fork复制出子进程的内存空间后,子进程内存空间的代码和数据和父进程是相同的,这样没有太大的意义,我们需要在子进 程空间里面运行全新的代码,这样才有意义。
有了exec后,我们可以单独的另写一个程序,将其编译好后,使用exec来加载即可。
3.2 exec函数族
exec的函数有很多个,它们分别是execve、execl、execv、execle、execlp、execvp,都是加载函数。
其中execve是系统函数,其它的execl、execv、execle、execlp、execvp都是基于execve封装得到的库函数,
3.2.1 execve函数原型
#include <unistd.h> int execve(const char *filename, char **const argv, char **const envp);
3.2.2 exec的作用
将新程序代码加载(拷贝)到子进程的内存空间,替换掉原有的与父进程一模一样的代码和数据,让子进程空间运行全新的程序。
3.2.3 在命令行执行./a.out,程序是如何运行起来的
(1)窗口进程先fork出子进程空间
(2)调用exec函数加载./a.out程序,并把命令行参数和环境变量表传递给新程序的main函数的形参
3.2.4 双击快捷图标,程序是怎么运行起来的
(1)图形界面进程fork出子进程空间
(2)调用exec函数,加载快捷图标所指向程序的代码
以图形界面方式运行时,就没有命令行参数了,但是会传递环境变量表
4. system函数
如果我们需要创建一个进子进程,让子进程运行另一个程序的话,可以自己fork、execve来实现,但是这样的操作很麻烦
所以就有了system这个库函数,这函数封装了fork和execve函数
调用时会自动的创建子进程空间,并把新程序的代码加载到子进程空间中,然后运行起来。
#include <stdlib.h> int system(const char *command);
(1)功能:创建子进程,并加载新程序到子进程空间,运行起来。
(2)参数:新程序的路径名
5. 回收进程资源
进程运行终止后,不管进程是正常终止还是异常终止的,必须回收进程所占用的资源。
5.1 为什么要回收进程的资源?
(1)程序代码在内存中动态运行起来后,才有了进程,进程既然结束了,就需要将代码占用的内存空间让出来(释放)。
(2)OS为了管理进程,为每个进程在内存中开辟了一个task_stuct结构体变量,进程结束了,那么这个结构体所占用的内存空间也需要被释放。
(3)等其它资源
5.2 由谁来回收进程资源
由父进程来回收,父进程运行结束时,会负责释放子进程资源。
- R 正在运行
- S 处于休眠状态
- Z 僵尸进程,进程运行完了,等待被回收资源
5.3 僵尸进程和孤儿进程
ps查看到的进程状态
5.3.1 僵尸进程
子进程终止了,但是父进程还活着,父进程在没有回收子进程资源之前,子进程就是僵尸进程
为什么子进程会变成僵尸进程?
子进程已经终止不再运行,但是父进程还在运行,它没有释放子进程占用的资源,所以就变成了占着不拉屎僵尸进程。
就好比人死后不腐烂,身体占用的资源得不到回收是一样的,像这种情况就是所谓的僵尸。
5.3.2 孤儿进程
没爹没妈的孩子就是孤儿,子进程活着,但是父进程终止了,子进程就是孤儿进程。
为了能够回收孤进程终止后的资源,孤儿进程会被托管给我们前面介绍的pid==1的init进程
每当被托管的子进程终止时,init会立即主动回收孤儿进程资源
回收资源的速度很快所以孤儿进程没有变成僵尸进程的机会。
6. wait函数
作用:父进程调用这个函数的功能有两个
- (1)主动获取子进程的“进程终止状态”。
- (2)主动回收子进程终止后所占用的资源。
wait函数,在实际开发中用的很少
6.1 进程的终止
6.1.1 正常终止
(1)main调用return
(2)任意位置调用exit
(3)任意位置调用_exit
不管哪种方式来正常终止,最终都是通过_exit返回到OS内核的。
6.1.2 异常终止
如果是被某个信号终止的,就是异常终止。
(1)自杀:自己调用abort函数,自己给自己发一个SIGABRT信号将自己杀死。
(2)他杀:由别人发一个信号,将其杀死。
6.1.3 进程终止状态
(1)退出状态与“进程终止状态”
return、exit、_exit的返回值称为“进程终止状态”,严格来说应该叫“退出状态”,
return(退出状态)、exit(退出状态)或_exit(退出状态)当退出状态被_exit函数交给OS内核,OS对其进行加工之后得到的才是“进程终止状态”,父进程调用wait函数便可以得到这个“进程终止状态”。
(2)OS是怎么加工的?
1)正常终止
进程终止状态 = 终止原因(正常终止)<< 8 | 退出状态的低8位
2)异常终止
进程终止状态 = 是否产生core文件位 | 终止原因(异常终止)<< 8 | 终止该进程的信号编号
(3)父进程调用wait函数,得到“进程终止状态”有什么用
父进程得到进程终止状态后,就可以判断子进程终止的原因是什么,如果是正常终止的,可以提取出返回值,如果是异常终止的,可以提取出异常终止进程的信号编号。
当有OS支持时,进程return、exit、_exit正常终止时,所返回的返回值(退出状态),最终通过“进程终止状态”返回给了父进程。
这有什么用,比如,父进程可以根据子进程的终止状态来判断子进程的终止原因,返回值等等,以决定是否重新启动子进程, 或则做一些其它的操作,不过一般来说,子进程的终止状态对父进程并没有太大意义。
6.2 父进程如何从内核获取子终止状态
6.2.1 如何获取
(1)父进程调用wait等子进程结束,如果子进程没有结束的话,父进程调用wait时会一直休眠的等(或者说阻塞的等)。
(2)子进程终止返回内核,内核构建“进程终止状态”
(3)内核向父进程发送SIGCHLD信号,通知父进程子进程结束了,你可以获取子进程的“进程终止状态”了。
6.2.2 wait函数原型
#include <sys/types.h> #include <sys/wait.h> pid_t wait(int *status);
(1)功能:获取子进程的终止状态,主动释放子进程占用的资源
(2)参数:用于存放“进程终止状态”的缓存(3)返回值:成功返回子进程的PID,失败返回-1,errno被设置。
6.2.3 从进程终止状态中提取进程终止的原因、返回值或者信号编号
(1)进程状态中所包含的信息
(2)如何提取里面的信息
系统提供了相应的带参宏,使用这个带参宏就可以从“进程终止状态”中提取出我们要的信息。
提取原理:相应屏蔽字&进程终止状态,屏蔽掉不需要的内容,留下的就是你要的信息。
(3)wait的缺点
如果父进程fork创建出了好多子进程,wait只能获取最先终止的那个子进程的“终止”状态,其它的将无法获取,
如果你想获取所有子进程终止状态,或者只想获取指定子进程的进程终止状态,需要使用wait的兄弟函数waitpid,
它们的原理是相似的
6. 进程状态
每个进程与其它进程并发运行时,该进程会在不同的“进程状态”之间进行转换
7. java进程
如何运行编译型和解释型语言的程序
(1)java程序的运行
1)父进程(命令行窗口、图形界面)会fork复制出子进程空间
2)调用exec加载java虚拟机程序,将虚拟机程序的代码拷贝到子进程空间中
其实最简单的理解就是,java虚拟机就代表了java进程。
当你运行另一个java程序时,又会自动地启动一个虚拟机程序来解释java字节码,此时另一个java进程又诞生了。
也就是说你执行多少个java进程,就会运行多少个java虚拟机,当然java虚拟机程序在硬盘上只有一份,只不过被多次启动而已。
(2)java虚拟机怎么得到
当我们运行java程序时,虚拟机会被自动启动。
虚拟机一般是运行在OS上的,不过其实虚拟机也可以运行在没有OS的裸机上
(3)在java程序里面,也可以调用java库提供的类似的fork和exec函数,我们自己来创建一个java子进程,并执行新程
java库提供的类似的fork、exec函数,下层也是调用OS的fork、exec函数。
8.进程关系
进程间的关系,大致有三种,即父子关系、进程组关系、会话期关系。
8.1 父子关系
已有进程调用fork创建出一个新的进程,那么这两个进程之间就是父子进程关系,子进程会继承和父进程的属性。
8.2 进程组
8.2.1 什么是进程组
多个进程可以在一起组成一个进程组,其中某个进程会担任组长,组长进程的pid就是整个进程组的组ID。
8.2.2 进程组的生命周期
就算进程组的组长终止了,只要进程中还有一个进程存在,这个进程组就存在。
8.3 会话期关系
多个进程组在一起,就组成了会话期。
9. 守护进程
守护进程也被称为精灵进程。