一、操作系统如何进行管理
在学习进程之前得先了解一下操作系统是怎么样对软硬件的资源进行管理的。
首先,什么是管理?
举个例子,校长是怎么管理学生的,难道校长要跟每一个学生打交道吗?那自然不是,实际上校长只需要知道学生的信息就可以下达指令去管理学生了。所以管理的本质其实就是对数据的管理。
其次,是如何管理?
对于校长来说,学生的核心数据无非是学号、年龄、姓名、性别、成绩等等之类的信息,这些信息足以描述一个学生了。同理,对于一本书来说,书名、价格、出版社、出版日期这些信息就足够大致描述一本书了。所以在管理前应当先描述对象,再组织信息。描述就是提出 描述对象 所需要的相关属性并使用结构体封装,再通过数据结构的形式组织起来。总结就是先描述,再组织。
所以操作系统管理软硬件资源就是把软硬件资源用结构体描述,在使用数据结构的形式组织。这也是为什么说linux一切皆文件的原因。
二、进程与PCB
进程与PCB的定义
进程,指一个正在执行的程序,更准确的说是一个正在执行的程序实列。
PCB又叫进程控制块,进程控制块就是描述进程的结构体。在Linux中,PCB具体的名字叫做task_struct。当程序加载到内存中执行时,操作系统就会分配一个PCB,task_struct含有所以关于进程的信息,所以Linux操作系统对进程的管理就转化成对task_struct的管理(对task_struct链表的增删查改)。
进程(组成)=PCB+自己的代码与数据
task_struct
task_struct包含内容如下:
标识符 | 标识进程,用于区别其他进程 |
状态 | 进程有多种状态,如运行、阻塞、挂起 |
优先级 | 执行进程的相对优先顺序 |
程序计数器 | 程序中即将执行的下一条指令的地址 |
内存指针 | 指向进程代码及数据的指针(还有和其他进程共享的内存块指针) |
上下文信息 | 进程执行时CPU的寄存器中的数据 |
IO状态信息 | 包括显示的I/O请求,分配给进程的I/O设备和正在被进程使用的文件列表 |
记账信息 | 可能包括处理器时间总和,使用的时钟总数,时间限制,记账号等 |
fork函数
作用:创建子进程(子进程从fork函数后面一句代码开始运行)
每一个进程都有唯一标识符(PID),用于区别进程。PPID是其父进程的PID。
- 子进程返回0
- 父进程返回子进程的PID(返回子进程PID便于父进程对其进行管理)
getpid函数 / getppid函数
获取当前进程的PID/获取当前进程的PPID。
子进程与父进程共用一份代码、数据(继承父进程的代码及数据),但数据的共用仅限于 读,一旦子进程对数据进行 写 操作就会发生写时拷贝。
ps指令查看进程信息
- ps aux 查看系统中所有的进程信息
- ps axj 可以查看进程的父进程号
内建命令
在Linux中绝大部分普通命令通常是bash交由其子进程去完成,而内建命令是指由bash亲自去执行的命令。
bash是最常用的一种shell(shell是运行在终端中的文本互动程序),是当前大多数Linux发行版的默认Shell。
父进程等待
为什么要等待?
等待是为了回收已完成的子进程。而回收主要干了两件事,一是了解子进程任务的完成情况(了解是否完成,失败了又是因为什么失败。本质:接收退出码、退出信息),二是释放子进程的PCB占用的空间(子进程的PCB没有立刻释放就是因为要从中读取退出码,退出信息)。
进程的终止
进程终止无非三种情况:
- 代码跑完,结果正确
- 代码跑完,结果错误
- 代码没跑完,出现异常,提前退出
对于第一第二种情况,通常用退出码来标识。而在退出码中,一般0表示程序正常,非0表示程序出错(非0的数如1,2,3,4等等,他们一方面表示错误,不同的数表示不同的错误。那么什么样的错误用什么样的数字表示呢?答案是可以使用默认的,也可以使用自定义的)
第三种情况,本质上是你的进程做了些不应该做的事,触发了操作系统的保护机制,于是操作系统发送信号杀死了你的进程以保证安全。此时退出码就没有意义了(退出码有意义的前提是程序执行完成)要去查看存储的退出信息。
如何终止?
- main函数的 return 为终止(其余函数的return就是单纯的返回)
- 代码调用exit()函数,无论在哪个函数中调用都是直接终止进程
- 操作系统中的_exit()函数 (与exit()不同的是,exit()会刷新缓冲区,而_exit()不会)
wait/waitpid函数
- pid_t wait (int* status) //等待任意子进程退出
- pid_t waitpid (pid_t pid , int* status , int options) //等待指定子进程退出
pid_t wait (int* status)
如果子进程没有退出,那么父进程就会处于阻塞状态等待其退出。等待成功则返回子进程PID,失败则返回负数。
status 参数是一个输出型参数,由操作系统填充。 如果传递NULL,表示不关心子进程的退出状态信息。如果不为空, 操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status的理解拓展
status比较复杂,虽然是整形但实际上用到的只有低字节序的2个字节。
例如:
- 如果子进程异常退出,则高8位的子进程的返回值是0,低8位中的低7位的异常退出信号值不为0
- 如果子进程正常退出,则高8位是子进程的返回值,低8位中的低7位是0
pid_t waitpid (pid_t pid , int* status , int options)
等待成功返回对应子进程PID,等待失败返回负数,检测成功但子进程还没退出返回0。
- 参数pid有两种情况,如果为 -1 则表示等待任意子进程,跟 wait() 函数一样。如果大于0,则表示等待指定子进程退出。
- 参数options提供一些控制waitpid()的选项。对于参数options如果不想使用可以传入0。参数(1)WNOHANG :如果指定的子进程没有结束,则waitpid()函数立即返回0,而不是阻塞在这个函数上等待;如果结束了,则返回该子进程的PID。参数(2)WUNTRACED:如果子进程进入暂停状态,则马上返回。
非阻塞等待
非阻塞等待顾名思义就是别干等着,一边等一边做点别的事。
OS调度算法
- 并发:指多个进程在一个CPU下采用进程切换的方式,在一段时间内,让多个进程都得以推进。
- 并行:指多个处理器或者是多核的处理器同时处理多个不同的任务。
并发在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行。每个进程都只执行很短一段时间,然后不管是否完成都将其置于队尾重新等待,然后马上执行下一个进程。(若是完成则离开队伍)
(红线是正在执行)
三、进程状态
ps aux 或者 ps axj 查看进程状态
进程状态分类
R 运行状态
处于这个状态的进程有的在CPU正在被执行,但更多的是在等待运行。所以准确的说R状态应该是一个允许被调度的状态。
S 睡眠状态
此时进程处于浅睡眠状态(正在被阻塞),等待到某种条件后就可以运行了。(这种状态下可以被信号唤醒、杀死)
D 磁盘休眠状态
此时进程处于深度睡眠状态,只能等待进程自动唤醒,(这种状态下信号无法杀死、唤醒进程)
D状态存在是为了防止一个进程正在对(IO)外设读取或写入时被操作系统杀死,导致数据丢失。
T 停止状态
可以通过发送 SIGSTOP 信号给进程来停止进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
- kill -SIGSTOP PID 停止进程
- kill -SIGSONT PID 继续进程
X 死亡状态
进程停止运行,无法投入运行(kill -9 PID杀死进程时就会出现,接收到SIGSTOP、SIGTSTP、SIGTTIN、SIGOUT等信号时同样会出现)
孤儿进程
如果父进程在子进程退出前退出,此时这个子进程就叫做孤儿进程。当然操作系统不会允许资源的浪费,所以会让 1号进程(名为 init进程 ,是所有进程的祖先) 领养他。这个孤儿进程的PCB空间释放就交给这个1号进程了。
让父进程先退出:
每隔1秒查看一次状态:
while :; do ps axj | head -1 && ps axj | grep a.out | grep -v grep; sleep 1; done
僵尸进程
僵尸进程:指该进程已经结束了,但是父进程却对其不管不顾,没有去wait()该进程,没有人去回收他导致其一直处于僵死的状态。(Z状态)
弊端:没有人知道任务完成得怎么样,也没有人去回收他的PCB块导致空间泄露。
注意:因为僵尸进程已经运行完成,即已经死亡,所以无法被操作系统杀死(死透了怎么可能还被杀死)
模拟实现:
那为什么平时父进程没有等待也没看见出现僵尸进程?
因为父进程退出后,该进程就从僵尸进程变成了孤儿进程,被1号进程领养并回收了。所以杀不死僵尸进程没关系,杀死父进程一样能解决僵尸进程(给他换个称职的父进程doge)。
四、进程优先级
什么是优先级?
指进程获得某种资源的先后顺序,在Linux中优先级数字越小优先级越高。
为什么要有优先级?
一个CPU一次只能执行一个进程,所以进程能访问的资源(CPU)是有限的,要合理分配资源。
操作系统关于调度和优先级的原则:分时操作系统,基本的公平(若进程长时间不被调度就会造成饥饿问题)
如何查看优先级
- ps -al
- PRI:进程优先级
- NI:进程优先级修正数据(nice值,新的优先级 = 优先级 + nice)
注意:nice并不是可以随意修改的,他的取值仅在[-20,19]中。这是为了保持 基本 公平的原则,防止资源分配完全乱套了。还有,修改了之后再次修改并不会在修改过一次的优先级基础上修改,而是以PRI为起点来修改。
Linux调度算法
五、环境变量
定义:环境变量一般是指在操作系统中用来指定操作系统运行环境的一些参数。
作用:环境变量是为了在执行命令的时候能找到其对应路径,可以不必写出命令的绝对路径。
查看方法
- echo $NAME (NAME为环境变量名)
查看PATH环境变量
添加环境变量
(1)添加到已存在目录
将test可执行程序拷贝到 PATH 中已经存在的工作路径下(例如:/usr/bin/),这样运行test时就不需要在其前面加上 ./ 了。
- sudo cp test /usr/bin/
弊端: 会污染其他工作目录,不利于长期发展。
(2)向PATH中添加新的目录来存放自己的程序
- export PATH = $PATH:/home/swi/test
弊端:Linux每次启动都会从 .bash_profile 中读取环境变量,这样设置重新启动linux时之前的设置就会被覆盖,导致失效。所以想要设置的环境变量永久生效,就得修改.bash_profile文件。
六、地址空间
虚拟地址
前面我们提到子进程与父进程共用一份代码、数据(继承父进程的代码及数据),但数据的共用仅限于 读,一旦子进程对数据进行 写 操作就会发生写时拷贝。
同样的地址,val却是两个不同的值,说好的写时拷贝呢?
实际上这个地址是虚拟地址,他们在为物理内存中的存储位置不一样。
虚拟地址的意义:
- 将无序变为有序,让进程以统一的视角看待物理内存以及自己运行的各个区域
- 进程管理模块和内存管理模块进行解耦
- 拦截非法请求(保护物理内存)
页表补充:
访问实际地址时,OS识别到错误时:
- 先检查是不是数据不在物理内存(缺页中断),如果是则加载到内存
- 检查是不是要进行写时拷贝,如果是则进行写时拷贝
- 如果都不是则进行异常处理(保护物理内存)
七、进程替换
如何替换?
直接替换PCB控制块中的代码及数据,对于PID、页表等不会改变。(有点类似于夺舍,外壳上来看还是原来的进程,但其中的代码和数据早就改变了)因为替换后代码就变了,原来剩余没执行的代码直接没有了。
execl
返回值:进程替换失败返回-1
- int execl(const char* path , const char* arg , ... ) 例子处蓝色字体部分对应参数 ...
path为可执行程序的路径,arg为如何执行这个可执行程序,而.....是指命令行参数,要以NULL标记结尾。
例如:execl("/usr/bin/ls","ls","-a","-l",NULL) 执行完这一个语句后,进程替换为ls指令
在命令行中是输入的是:"ls","-a","-l" 则函数传参中填入的是:"ls","-a","-l",NULL
如图:
execlp
返回值:进程替换失败返回-1
- int execlp(const char *file, const char *arg,…) ...的填写如上,若是没有则填NULL
execlp中的参数file与execl的参数path的区别:前者不需要写出路径,只需要写入要替换的程序名即可,操作系统会默认在Linux环境变量PATH中查找可执行程序。
例如:execlp("ls","ls","-a","-l",NULL)
execle
返回值:进程替换失败返回-1
- int execle(const char* path, const char* arg, …,char* const envp[ ])
envp数组为要导入的环境变量。(可以填NULL,但既然并导入环境变量,还是建议使用其余几个替换函数)
execv
- int execv(const char* path,char* const argv[ ])
argv数组就相当于前面出现过的 const char *arg和… ,argv数组里面存储的就是const char *arg和…的值。
例如:
int main()
{
const char argv = {"ls","-a","-l",NULL};
execv("/usr/bin/ls",argv);
return 0;
}
execvp、execve
- int execvp(const char *file, char * const argv []);
- int execve(const char *file, char *const argv[])
函数名解析
替换函数前面的exec不变:
- l:参数采用列表
- v:参数采用数组
- p:不需要输入路径,在环境变量自动搜索
- e:要导入自己的环境变量
参数名解析
参数名 | 含义 |
---|---|
file | 替换的程序名 |
bath | 可执行程序的路径以及替换的程序名 |
argv 等价于 const char *arg和… | 替换的程序名 + 可选选项 + NULL |
envp | 要导入的环境变量 |
总结
- int execl(const char* path , const char* arg , ... )
- int execlp(const char *file, const char *arg,…)
- int execv(const char* path,char* const argv[ ])
- int execle(const char* path, const char* arg, …,char* const envp[ ])
- int execvp(const char *file, char * const argv [])
- int execve(const char *file, char *const argv[])
注意: 不仅仅可以用来替换成系统指令,也可以替换成自己写的代码。
例如:execl("./test" , "test" , NULL )
test.c文件(生成的可执行文件名为test):
rep.c文件(生成的可执行文件名为rep):
执行结果: