前言
Vue框架:
Vue驾校-从项目学Vue-1
算法系列博客友链:
神机百炼
进程调度队列runqueue:
优先级数组:
-
调度队列不只一个队列,根据优先级划分,共有40+100个队列:
当一个优先级所拉链出来的双链表内进程都调度过后,开始遍历下一优先级所拉链出来的双链表
位图:
-
位图:
一共有40+100个优先级,每个优先级上有待调度的进程则用1记录,没有待调度的进程则用0记录
由于优先级数组queue[]一共140个,所以至少需要140bit才能记录完全每个优先级上是否有需要执行的进程
开一个32*5大小的变量bit[5],其中第i位上0上1表示queue[i]上是否含有待执行进程
nr_active:
-
含义:
当前queue[140]下共有多少运行状态下的进程
活动队列:
-
含义:
当前时间片未耗尽,正在等待调度的进程们构成的调度队列
这些在等待调度的进程们也是由优先级数组queue[]通过拉链双链表来组织的
过期队列:
-
含义:
当前时间片已经耗尽,暂时不会再被调度的进程们构成的调度队列
这些暂不会被调度的进程们也是由优先级数组queue[]通过拉链双链表来组织的
active指针& expired指针:
-
含义:
active指向活动队列,当活动队列中所有进程时间片耗尽后active指向原本expried指向的过期队列
expired指向过期队列,当活动队列中所有进程时间片耗尽后expired指向原本active指向的活动队列
进程调度算法时间复杂度:
-
O(1):
根据位图中为1的位,
查找待角度进程的queue[i],
之后遍历queue[i]拉出的PCB双链表,
执行对应进程即可
内存中的进程调度结构体:
- 图示:
进程创建:
- 在初识进程中初步使用和了解了fork()函数,经过对进程地址空间和进程状态的学习,我们重新审视该函数:
fork():
-
为什么有两个返回值?
- 父进程返回所创建的子进程的pid
- 子进程返回0
- 创建失败返回-1
-
实例:
-
代码:
int main(){ pid_t pid; printf("Before: pid is %d\n", getpid()); if ( (pid=fork()) == -1 ) perror("fork()"),exit(1); //perror()为手动报错 printf("After:pid is %d, fork return %d\n", getpid(), pid); sleep(1); return 0; }
-
输出:
[root@localhost linux]# ./a.out Before: pid is 43676 After:pid is 43676, fork return 43677 After:pid is 43677, fork return 0
-
解释:
- fork()之前的代码只有父进程执行
- fork()之后的代码父子进程都执行,谁先取决于进程调度器
-
mm_struct:
-
虚拟地址也称为线性地址,从0x 0000 0000到0x ffff ffff
其内部区域的划分其实也是通过结构体实现的:
-
虚拟地址空间划分结构体:mm_struct{}
struct mm_struct{ unsigned int code_start; //正文代码区 unsigned int code_end; unsigned int readonly_start; //字符串常量区 unsigned int readonly_end; unsigned int init_start; //初始化数据区 unsigned int init_end; unsigned int uninit_start; //未初始化数据区 unsigned int uninit_end; unsigned int heap_start; //堆区:向下生长end++ unsigned int heap_end; unsigned int stack_start; //栈区,向上生长start-- unsigned int stack_end; };
-
可执行程序:
- 程序文件通过编译器被编译为可执行文件时,已经划分好了这6大区域
- 可执行文件从硬盘转移到内存中时,6大区域直接照搬即可
- exe文件的具体格式称作ELF
-
页表:
数据最终是存在内存上的物理地址的,而每个进程对于虚拟内存的使用情况不同,所以每个进程的物理地址和虚拟地址的映射关系不同
也就是说每个进程的页表不同,需要和mm_struct配套单独创建
写时拷贝:
-
前一篇中我们讲了父子进程原本共享相同数据和程序
当子进程想要修改数据时,为了保证父进程的独立性,要为子进程单独新开一片存储修改了的数据的内存
再把新开内存的物理地址和虚拟地址建立新的映射关系,加载到页表中
这个过程就叫做写时拷贝
-
图解写时拷贝:
页表+虚拟地址的作用:
-
防止直接接触OS,保护内存
- 一方面:进程只能访问页表内存在映射的物理地址,绝对不可能越界访问
- 另一方面:就算进程尝试越界访问野指针时,页表发现本进程不可访问该地址,在进程对该地址操作前就终止了异常进程
-
统一化内存管理
-
每个进程可操作的内存空间统一都是0x0000 0000 ~ 0xffff ffff
对每个进程的内存分配处理都大体相同
-
-
维护进程独立性
-
每个进程在运行时,都认为自己占据着所有的资源
-
实现进程调度和内存管理解耦:
程序分段加载到内存的物理地址中,这个过程是独立的
页表将物理地址和虚拟地址建立映射,这个过程也是独立的
进程访问虚拟地址,这个过程也是独立的
-
进程创建时创建的内容:
- 程序+数据从硬盘转移到内存
- PCB(task_struct)块创建到内存中
- mm_struct创建到内存中
- 页表
- PCB加入双链表(可能也加入了调度队列)
进程终止:
-
进程退出的三种情况:
- 代码运行正常,结果正确
- 代码运行正常,结果错误
- 代码运行异常
-
前文我们讲僵尸进程时已经讲过程序调用关系:
OS调用加载器,加载器调用mainCRTStartup(),mainCRTStartup()调用main()函数
最终main()的return返回给了OS的进程退出码$
echo $? //打印最近一次进程退出时的进程退出码
-
进程
main()函数的return():
-
只有main()函数自身的return,才能将值赋予OS的进程退出码
-
代码:
-
输出:
-
exit():
-
exit(n):
随处执行随处退出进程,且进程退出码为n
退出后会执行后续工作:关闭输入输出流/刷新缓冲区/执行可能有的clean操作
-
举例:
-
代码:
-
输出:
-
_exit():
-
_exit(n):随处执行随处退出进程,且进程退出码为n
-
与exit()区别:
不会执行后续工作:刷新缓冲区/关闭输入输出流/执行可能有的clean操作
-
区别图解:
进程退出内存过程:
- 删除内存中的附属信息:
- PCB结构体块:task_struct{}
- 进程虚拟地址空间布局:mm_struct{}
- 页表
- 删除双链表中的PCB节点:
- PCB双链表的节点
- runqueue[]中queue[]中双链表的节点
进程异常情况集strerror():
-
进程的异常情况一共有150种,都存储在了strerroe()函数中:
-
代码:
-
输出:
-
进程等待:
含义:
-
父子进程谁先运行?
运行顺序取决于进程调度算法
-
父子进程谁先结束?
一方面,为了防止“孤儿进程”,一般都是子进程先结束
另一方面,僵尸进程只能通过父进程/OS领养,回收其数据后将进程退出,无法kill -9
这就意味着就算父进程已经执行完所有任务,最终也需要等待子进程退出后回收其数据
-
进程等待:
子进程运行时,父进程单纯在等,等待回收子进程资源&获取子进程退出信息
-
父进程等待成功是否意味着子进程执行成功?
不是,
- 子进程可能执行异常,结果返回异常信息
- 子进程可能执行顺利,返回正确结果
- 子进程可能执行顺利,返回错误结果
但凡子进程执行完毕,不论是否结束,父进程都要等待回收子进程资源&获取子进程退出信息
wait() & waitpid():
-
wait():在众多子进程中随机选择一个,返回其退出情况
#include<sys/types.h> #include<sys/wait.h> pid_t wait(int*status); //输出型参数:status /*返回值: 成功返回被等待进程pid,失败返回-1。 参数: 输出型参数,获取子进程退出状态;若不关心子进程退出情况则设置为NULL即可 */
-
waitpid():指定一个子进程Pid,返回其退出情况
pid_ t waitpid(pid_t pid, int *status, int options); //输出型参数:status /* 返回值: 1,指定子进程运行完毕:返回子进程pid 2,指定的子进程不存在:返回0 3,调用中出错:返回-1,errno会被设置成相应的值以指示错误所在 参数: 1,pid:指定子进程pid Pid=-1,等待任一个子进程。此时waitpid()与wait()等效。 Pid>0.等待其进程ID与pid相等的子进程。 2,status:进程退出结果 != 进程退出码 WIFEXITED(status): 进程正常退出则返回1,进程异常则返回0 WEXITSTATUS(status): 返回进程退出码(退出码只对正常退出的进程有用) 3,options:决定是否等待结果 WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。 若正常结束,则返回该子进程的ID。 */
-
使用实例:
-
代码:
-
输出:status不是0~149之间的错误码,而是2816,说明status和退出码$有区别
-
进程退出结果status与进程退出码$:
-
进程退出结果status本质是一个位图:
8位退出码 + 1位core dump + 7位终止信号:
-
查看终止信号:
进程运行时发生异常,导致进程收到了终止信号
status & 0x7f
-
查看进程退出码$:
只有status最低7位是0时,说明进程是正常退出的,此时$才有参考意义
(status >> 8) & 0xff
-
查看正常退出进程的stutas $ 信号:
-
代码:kill -l展示所有信号
-
输出:
-
-
向进程发送信号,查看其status $ 信号:
-
查看异常运行的进程的status $ 信号:
-
代码:除以0
-
输出:
-
-
查看存在野指针的异常进程的status $ 信号:
-
代码:
-
输出:
-
批量创建并查看子进程:
-
代码:用数组保存子进程号
int main(){ pid_t idx[10]; //创建子进程 for(int i=0; i>10; i++){ pid_t id = fork(); if(id == 0){ for(int i=0; i<10; i++) printf("子进程 %d %d\n", getid(), getppid()): exit(1); //子进程结束 } idx[i] = id; //只有父进程执行 } int status = 0; for(int i=0; i<10; i++){ pid_t res = waitpid(idx[i], &status, 0); if(ret >= 0){ printf("子进程%d 等待结束\n", ret); printf("子进程退出状态:%d\n", status); printf("子进程退出码$:%d \n", (status>>8)&0xFF); printf("子进程信号:%d \n", status&0x7f); } } return 0; }
宏查看$和信号:
-
上述过程使用wait() / waitpid()接收status后,还需要手动移位和与
但是其实可以使用官方给定的宏来解析获取到的status内的信息
WIFEXITED:
-
作用:查看所等待的子进程是否正常退出
-
使用方式:搭配wait()/waitpid()获得status
int status; pid_t ret = waitpid(dix[0], &status, 0); if(WIFEXITED(status)){ printf("child exit normally\n"): }else{ printf("child exit error\n"); }
WEXITSTATUS:
-
前提:WIFEXITED返回值为真(进程无异常)
-
作用:查看所等待的子进程的退出码
-
使用方式:搭配wait()/waitpid()获得status
int status; pid_t ret = waitpid(dix[0], &status, 0); if(WIFEXITED(status)){ printf("child exit code:%d\n",WEXITSTATUS(status)): }else{ printf("child exit error\n"); }
进程阻塞 & 进程非阻塞:
-
进程阻塞:
父进程在等待回收子进程僵尸状态时的资源和数据时,什么操作也不执行,一直关注子进程是否终止
-
进程非阻塞:
父进程在等待回收子进程僵尸状态时的资源和数据时,运行自己的其他程序
每过一定时间,去查询一下子进程是否运行结束
-
阻塞/非阻塞等待模式的代码写法:
//进程阻塞:
waitpid(id, &status, 0);
//进程非阻塞:
waitpid(id, &status, WNOHANG);
//W含义wait,NO含义没有,HANG含义阻塞
-
进程非阻塞模式:
-
代码:
#include <stdio.h> #include <unistd.h> #include <sys/wait.h> #include <sys/types.h> int main(){ pid_t id =fork(); if(id ==0){ for(int i=0; i<20; i++){ printf("I am child, pid: %d, ppid: %d\n", getpid(), getppid()); sleep(3); } exit(1); } while(1){ int status = 0; pid_t ret = waitpid(id, &status, WNOHANG);//WNOHANG为非阻塞,HANG表示阻塞 if(ret > 0){ //子进程回收,父进程结束等待 printf("wait success!\n"); printf("exit code: %d\n", WEXITSTATUS(status)); break; }else if(ret == 0){ //子进程未终止,父进程继续等待 printf("father do other things!\n"); }else{ //子进程异常 printf("waitpid error!\n"); break; } } return 0; }
-
输出:
-
进程程序替换:
-
背景:
子进程的程序和数据默认直接利用父进程
偶然的局部数据改动通过写时拷贝来区别于父进程
进程的程序替换就是要将子进程的所有程序和数据都从硬盘新导入,和父进程程序与数据根本没有联系
-
进程不变:
程序替换的时候没有创建子进程
PCB mm_struct 页表 都没有新建
只是PCB中对程序和数据的指针指向发生改变
-
程序替换:由于替换的都是0101的可执行文件,所以不同语言之间都可以执行进程替换
进程程序替换函数:
-
程序加载器:
- 作用:将硬盘中的文件加载到内存中执行
- exec系列函数底层其实就是程序加载器
-
六大替换函数:替换失败统一返回-1
#include <unistd.h>` //path为硬盘上的可执行程序路径,可以提前使用which查询 //arg为参数 //...意为可变参数源,意思是想传几个参数就传几个参数,但是必须以手写NULL结尾 int execl(const char *path, const char *arg, ...); int execlp(const char *file, const char *arg, ...); int execle(const char *path, const char *arg, ...,char *const envp[]); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]);
-
父进程使用举例:
-
代码:
-
输出:程序替换execl()之前的程序还被执行,之后的程序已经被替换覆盖
-
异常:程序替换一旦失败,execl()后续的程序不会被覆盖,还会继续执行
-
-
程序调用函数的特点:
- 不需要返回值:一经调用,马上覆盖原程序,不需要return给原程序任何值
- 有返回值说明替换失败
- 一般搭配exit()使用,替换成功去执行新程序,替换失败原程序直接终止
-
子进程使用举例:
-
代码:
-
输出:
-
execl() & execv:
-
异同:
-
同:都是依据路径寻找到目标文件,再进行程序替换
-
异:
-
l表示参数以列表形式传入:
execl("usr/bin/ls", "ls","-a","-i","-l",NULL);
-
v表示参数以数组形式传入
char* argv[] = { "ls", "-a", "-i", "-l", NULL } execl("usr/bin/ls", argv);
-
-
execlp() & execvp():
-
异同:
-
同:默认依据环境变量PATH找到目标文件,不用带路径,但需要声明指令
-
异:
-
lp需要声明指令+列表携带参数
execlp("ls", "ls", "-a", "-i", "-l",NULL);
-
vp需要声明指令+数组携带参数
char* argv[] = { "ls", "-a", "-i", "-l", NULL } execvp("ls", argv);
-
-
execle() & execve():
-
异同:
-
同:都是依据指定路径寻找可执行文件,再通过调用程序传递自定义的“本地变量”
-
异:
-
le以列表携带参数:
char *env[] = { "MYENV=youcanseeme", NULL }; execle("./cmd","cmd",NULL,env); //./表示当前路径
-
ve以数组携带参数:
char *argv[] = { "cmd", NULL } char *env[] = { "MYENV=youcanseeme", NULL }; execle("./cmd",argv,env);
-
-
-
获取OS自带的 或 用户自定义的环境变量:
-
函数:
getenv(PATH); //获取OS自带的环境变量 getenv(自定义的环境变量名); //获取用户传递来的环境变量
-
代码:
-
输出:
-
未定义MYENV时:getenv(PATH)有效,getenv(MYENV)无效
-
定义了MYENV时:getenv(MYENV)有效,getenv(PATH)无效
-
-
makefile一次产生多个可执行文件:
-
错误写法:孤立依赖关系
-
正确写法:伪目标综合依赖关系