Vue框架:
Vue驾校-从项目学Vue-1
算法系列博客友链:
神机百炼
调度队列不只一个队列,根据优先级划分,共有40+100个队列:
当一个优先级所拉链出来的双链表内进程都调度过后,开始遍历下一优先级所拉链出来的双链表
位图:
一共有40+100个优先级,每个优先级上有待调度的进程则用1记录,没有待调度的进程则用0记录
由于优先级数组queue[]一共140个,所以至少需要140bit才能记录完全每个优先级上是否有需要执行的进程
开一个32*5大小的变量bit[5],其中第i位上0上1表示queue[i]上是否含有待执行进程
含义:
当前queue[140]下共有多少运行状态下的进程
含义:
当前时间片未耗尽,正在等待调度的进程们构成的调度队列
这些在等待调度的进程们也是由优先级数组queue[]通过拉链双链表来组织的
含义:
当前时间片已经耗尽,暂时不会再被调度的进程们构成的调度队列
这些暂不会被调度的进程们也是由优先级数组queue[]通过拉链双链表来组织的
含义:
active指向活动队列,当活动队列中所有进程时间片耗尽后active指向原本expried指向的过期队列
expired指向过期队列,当活动队列中所有进程时间片耗尽后expired指向原本active指向的活动队列
O(1):
根据位图中为1的位,
查找待角度进程的queue[i],
之后遍历queue[i]拉出的PCB双链表,
执行对应进程即可
为什么有两个返回值?
实例:
代码:
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
解释:
虚拟地址也称为线性地址,从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;
};
可执行程序:
页表:
数据最终是存在内存上的物理地址的,而每个进程对于虚拟内存的使用情况不同,所以每个进程的物理地址和虚拟地址的映射关系不同
也就是说每个进程的页表不同,需要和mm_struct配套单独创建
前一篇中我们讲了父子进程原本共享相同数据和程序
当子进程想要修改数据时,为了保证父进程的独立性,要为子进程单独新开一片存储修改了的数据的内存
再把新开内存的物理地址和虚拟地址建立新的映射关系,加载到页表中
这个过程就叫做写时拷贝
图解写时拷贝:
防止直接接触OS,保护内存
统一化内存管理
每个进程可操作的内存空间统一都是0x0000 0000 ~ 0xffff ffff
对每个进程的内存分配处理都大体相同
维护进程独立性
每个进程在运行时,都认为自己占据着所有的资源
实现进程调度和内存管理解耦:
程序分段加载到内存的物理地址中,这个过程是独立的
页表将物理地址和虚拟地址建立映射,这个过程也是独立的
进程访问虚拟地址,这个过程也是独立的
进程退出的三种情况:
前文我们讲僵尸进程时已经讲过程序调用关系:
OS调用加载器,加载器调用mainCRTStartup(),mainCRTStartup()调用main()函数
最终main()的return返回给了OS的进程退出码$
echo $? //打印最近一次进程退出时的进程退出码
进程
只有main()函数自身的return,才能将值赋予OS的进程退出码
代码:
输出:
exit(n):
随处执行随处退出进程,且进程退出码为n
退出后会执行后续工作:关闭输入输出流/刷新缓冲区/执行可能有的clean操作
举例:
代码:
输出:
_exit(n):随处执行随处退出进程,且进程退出码为n
与exit()区别:
不会执行后续工作:刷新缓冲区/关闭输入输出流/执行可能有的clean操作
区别图解:
进程的异常情况一共有150种,都存储在了strerroe()函数中:
代码:
输出:
父子进程谁先运行?
运行顺序取决于进程调度算法
父子进程谁先结束?
一方面,为了防止“孤儿进程”,一般都是子进程先结束
另一方面,僵尸进程只能通过父进程/OS领养,回收其数据后将进程退出,无法kill -9
这就意味着就算父进程已经执行完所有任务,最终也需要等待子进程退出后回收其数据
进程等待:
子进程运行时,父进程单纯在等,等待回收子进程资源&获取子进程退出信息
父进程等待成功是否意味着子进程执行成功?
不是,
但凡子进程执行完毕,不论是否结束,父进程都要等待回收子进程资源&获取子进程退出信息
wait():在众多子进程中随机选择一个,返回其退出情况
#include
#include
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本质是一个位图:
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内的信息
作用:查看所等待的子进程是否正常退出
使用方式:搭配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");
}
前提: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
#include
#include
#include
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的可执行文件,所以不同语言之间都可以执行进程替换
程序加载器:
六大替换函数:替换失败统一返回-1
#include `
//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()后续的程序不会被覆盖,还会继续执行
程序调用函数的特点:
子进程使用举例:
代码:
输出:
异同:
同:都是依据路径寻找到目标文件,再进行程序替换
异:
l表示参数以列表形式传入:
execl("usr/bin/ls", "ls","-a","-i","-l",NULL);
v表示参数以数组形式传入
char* argv[] = {
"ls",
"-a",
"-i",
"-l",
NULL
}
execl("usr/bin/ls", argv);
异同:
同:默认依据环境变量PATH找到目标文件,不用带路径,但需要声明指令
异:
lp需要声明指令+列表携带参数
execlp("ls", "ls", "-a", "-i", "-l",NULL);
vp需要声明指令+数组携带参数
char* argv[] = {
"ls",
"-a",
"-i",
"-l",
NULL
}
execvp("ls", argv);
异同:
同:都是依据指定路径寻找可执行文件,再通过调用程序传递自定义的“本地变量”
异:
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)无效
错误写法:孤立依赖关系
正确写法:伪目标综合依赖关系