什么是进程
windows的进程管理器中,可以看到所有运行的进程。 启动一个软件,本质就是启动了一个进程。 在linux中,运行一条命令,./xxx时,就是在系统层面创建了一个进程。
linux是可以同时加载多个程序的,Linux系统可能同时存在大量的进程在内存中!!!!
ps axj:查看系统中所有进程
linux要把这些进程管理起来 ------ 先描述,再组织!!!
相当多的程序都被加载到内存中,cpu要调度某一个加载到内存中的程序 ------ 哪一个执行完、哪一个应该被删除、哪一个优先级较高等,这些需要os管理起来
linux为每一个进程都创建一个pcb结构体,保存了该进程所有的属性 ------ task_struct
进程 = 对应的代码和数据 + os内核为了管理进程而创建的数据结构
例如:如果一个人是学校的学生,他要在学校,同时还要被学校管理起来。
如果人在学校就是学生,那么保安也是、图书馆管理员也是。所以除了要有对应的人,
个人的属性信息、籍贯、宿舍、入学时间等,都应该录入到学校的教学管理系统中。
对学生的增删查改就是对教学管理系统的增删查改,对进程的增删查改就变成了对进程相关数据结构的增删查改
PCB是什么,内部有什么属性?
pcb(process control block):进程控制块,在linux下叫 task_struct,用来描述一个进程的属性信息
以下是部分属性:
1 标识符:描述进程的唯一标识符,用来区别其他进程 PID,和学号一样
2 进程状态:标识进程正在运行,阻塞等
3 优先级:确定资源谁前谁后使用(排队打饭)
4 程序计数器:进程中即将被执行的下一条指令的地址
CPU怎么知道进程运行到哪一行代码?进程pcb里和cpu内都有个pc指针,保存了当前进程下一条指令地址。
5 内存指针:指向进程对应的代码和数据
6 上下文数据:进程被调度时,cpu寄存器中的数据
7 I/O状态信息:当前进程能使用哪些io设备,例如------该进程打开的文件列表
8 记账信息:累记被cpu处理了多长时间等
9 当前进程的所在工作目录cwd:进程在哪个路径被创建的
系统中查看进程
ps axj ------ 查看系统中所有进程
top ------ 相当于windows的任务管理器
ls /proc ------动态 我们当前运行的所有进程 都会以文件的形式 展示在/proc,多一个进程就会多一个目录,少一个进程就会少一个目录
例如:在我的工作目录运行死循环进程,查看该进程的属性信息
进程的父子关系
进程有父子关系
scss
pid_t getpid()//获取当前进程pid
pid_t getppid() //获取父进程pid
在命令行解释器中,后台运行多个死循环进程,会发现它们的父进程都是一样的,父进程就是bash(shell外壳程序)
scss
while(1){}
shell运行原理:在同一个终端,命令行下,父进程bash创建子进程去执行任务
关了bash,当前的终端就不能正常的进行命令行解释
另外,每一次登录,都会有新的bash
fork创建子进程
fork基本用法
pid_t fork()
1 作用:fork()以后,会有2个执行流
2 返回值
创建失败: 返回-1
创建成功: 给父进程 返回 子进程的pid;给子进程返回0
创建出来,是为了做不一样的事情,把任务拆解成让不同的执行流去执行,才能提高效率 ------ 让父子进程执行不同代码 ,所以用条件判断做不同的事情
在父进程里 id=子进程的pid,在子进程里 id=0
scss
int main()
{
pid_t id = fork();
if(id < 0)
{
//创建失败
perror("fork error");
return 1;
}
else if(id == 0)
{
//child process(task)
//父进程能看到,但他不能执行
while(1)
{
sleep(1);
printf("i am child, pid:%d,ppid:%d\n",getpid(),getppid());
}
}
else if(id > 0)
{
//parent process
while(1)
{
sleep(1);
printf("i am parent,pid:%d,ppid:%d\n",getpid(),getppid());
}
}
return 0;
}
为什么给子进程返回0,给父进程返回子进程的pid(用来记忆)
父进程:子进程 = 1:n
1个父亲,可能有多个小孩;1个小孩只有一个父亲
父亲给孩子取名字来标识一个小孩,fork之后,给父进程返回子进程的pid
子进程只有1个父进程,可以很方便找到 ------ getppid
fork调用失败原因
1 系统有太多进程,进程创建是要消耗内存资源的
2 当前用户的进程数量超过限制
进程状态
维护进程状态,本质是在更改task_struct里的status属性,用不同的数字标识
arduino
#define NEW 1
#define RUN 2
struct task_struct{
int status;
}
1 新建:初步具备了进程相关数据结构。字面意思(但linux内核中没有这个状态)
2 运行:task_struct结构体在运行队列中排队,就叫做运行态 !!!即等待cpu资源的队列
3 阻塞:系统中有各种资源,不仅仅是cpu(运行队列),还有网卡、文件、磁盘等其他设备
系统中不仅存在一种队列,还有等待其他资源的队列。
例如:等待磁盘资源就绪的队列
进程等待非cpu资源就绪时,要放到其他队列中,这种队列就是阻塞队列。
即放在阻塞队列中等待非cpu资源就绪的进程,就处于阻塞状态
arduino
int main(){
int a;
scanf("%d",&a);//等待键盘数据就绪 ./xxx.exe -> 运行变成进程,如果一直不输入,进程就卡在那里
//阻塞状态
return 0;
}
4 挂起:当内存不足时,os把进程的代码和数据适当换出到磁盘,进程状态就是挂起!!!!
例如:一个大型游戏128GB,os不可能把游戏进程的所有代码和数据放进内存。刚开始进主菜单时,只把游戏的主菜单代码和数据换入到内存执行;当进入游戏界面后,主菜单的代码和数据可以直接换出,当需要时再换入进内存。
5 挂起阻塞:进程正在等待某种非cpu资源,进程pcb被放到阻塞队列,同时它的代码和数据被置换到swap分区
linux具体进程状态
1 TASK_RUNNING对应上面的运行态,标识进程正在运行队列或者正在被cpu调度
csharp
int main(){
while(1){}
}
2 interruptible ------ 可中断睡眠,就是等待非cpu资源,对应上面的阻塞状态,可以用信号中断
arduino
int main(){
int a;
while(1){
scanf("%d",&a);//等待外设资源就绪
}
}
+号意味着这个任务属于前台进程------启动会占用命令行对话框(不能执行输入的命令),可以用ctrl+c终止
后台进程:运行进程时,后面带& ------ 不影响命令行交互
3 uninterruptible ------ 不可中断睡眠,不可被信号中断
理解:假设进程想把某一块数据,刷新到磁盘中。
进程A把数据交给磁盘,磁盘就在自己内部找对应位置写数据,磁盘是机械设备,很慢;
磁盘是否写成功,要告诉给进程A ------ 进程A要等磁盘结果,等待某种资源就绪,进程状态是阻塞状态。
正好在进程A等待期间,os收到了很多任务,有很多进程,os的内存非常紧张。
os看到进程A在休眠,等待磁盘资源就绪,os就可能把这个进程杀掉!!!!
服务器压力过大时,os会通过一定手段,杀掉一些进程来节省空间的作用
此时磁盘数据写失败了,想发结果给对应进程,可是进程已经被干掉了 ------ 数据丢失!!!
如果进程正在磁盘读写有关的,进程在阻塞,同时在阻塞期间,不允许os杀掉,就设为uninterruptible
只能等这个进程得到磁盘读写结果,读写成功或失败后,进程才能被唤醒.
4 stopped ------ 用信号kill -19 pid,向目标进程发送19号信号SIGSTOP,让进程停止执行了。
5 traced ------ 代码正在调试
6 exit_dead终止 ------ os面对许多个进程pcb,同一时间可能有大量的进程退出,系统释放资源得一个个来。
X状态一定要维持起来,让os能识别到可被回收,但os可能正忙着回收其他的进程或其他,所以这个状态要被维护
7 exit_zombie僵尸状态 ------ 进程退出,但是进程的资源还没有被父进程回收,例如运行结果
僵尸进程
一个进程已经退出,但是还不允许os释放,处于一个被检测的状态,叫做僵尸状态!!!!!!!
一个进程是否正常退出、允许结果等,谁来关心呢?一般父进程或者os(父进程也挂了的情况下)
fork()创建子进程,为什么要创建子进程?因为要让他可以执行不一样的代码,完成别的事情。那子进程退出了,父进程想不想知道子进程把事情做得怎么样??当然,也可能不关心。
维持该状态,是为了让父进程或os回收子进程的退出结果(子进程办的怎么样,异常还是正常退出)
scss
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进程先退出
}
else if(id > 0)
{
while(1){}
}
else if(id < 0)
{
perror("fork");
exit(1);
}
return 0;
}
一个进程处于僵尸状态,数据结构pcb没有回收,代码和数据可以被释放,
但它的数据结构会占据内存 ------ 内存泄漏
孤儿进程
父进程如果提前退出,子进程后退出,进入Z状态后,如何处理?
父进程先退出,子进程还在,子进程就称为孤儿进程!!!
孤儿进程被1号Init进程领养,当然由init进程回收
scss
int main()
{
pid_t id = fork();
if(id == 0)
{
while(1){}
}
else if(id > 0)
{
//父进程先退出
}
else if(id < 0)
{
perror("fork");
exit(1);
}
return 0;
}
进程优先级
为什么要有优先级?
因为cpu是有限的,但进程太多。需要通过某种方式竞争资源
什么是优先级
确认是谁先获得某种资源,谁后获得。
我们是可以用一些数据来表明优先级.
优先级是调度器调度的主要参考
调度:把谁先从运行队列中放到cpu上,把谁将cpu上拿下来
linux具体的优先级做法
ps -al 查看当前会话中相关进程 PRI:代表这个进程可被执行的优先级,其值越小,优先级越高,越早被执行priority
NI:NICE值,优先级的修正数值
优先级 = 老优先级 + nice值
默认每一次设置优先级,老的优先级都是80
这样NICE为负值时,优先级就会提高,越快被执行
所以,调整优先级,在linux下就是调整NICE值
如何修改进程的优先级
1 top -> 输入r -> 输入进程pid -> 要设置的NICE值
2 nice值的取值范围[-20,19]
3 由于nice的取值范围,以及老的优先级默认都是80,linux下创建的进程优先级范围[60, 99]
4 如果允许优先级调整范围过大,就可能有些恶意进程恶意调整自己的优先级,导致cpu调度器总先调度 它,导致其他进程长时间得不到调度。
重要概念
竞争性
进程数目众多,cpu资源有限,需要竞争资源,因此进程之间有竞争性
独立性
进程之间相互独立,互不影响。
例如:画图板崩了,不会影响xshell客户端。
例如:父进程挂了,不会影响子进程。
并行
电脑上有多个cpu
在任何一个时刻,同时有2个以上的进程在被调度运行 ------ 并行
并发
什么是并发?
大部分情况下,电脑只有一个cpu
os在进行调度时,在运行队列中选择特定的进程,放到cpu上去运行
任何一个时刻都只有一个进程在跑
但在一个时间段内,基于时间片、基于切换、基于抢占和出让
多个进程的代码都得以推进,这就是并发
并发如何实现的?
1 时间片:
给每个进程相对固定的时间,让每个进程都要在特定的时间段内,较为均衡占用cpu资源
如果时间片没到,进程就执行完了,进程会立刻从cpu下来;
例如:A进程是个死循环,但它在cpu上占用10ms后必须下来,os选择下一个进程继续运行
再比如:A进程放到cpu上,不可能一直占有cpu资源,例如只给10ms,10ms后不管运行了多少,都必须从cpu上下来。
2 抢占和出让:
抢占 ------ 优先级较高的进程抢占正在cpu上跑的进程
出让 ------ 把自己从cpu上拿下来,放到其他队列中
3 切换:
如果进程A正在被运行,cpu的寄存器里,一定保存的是进程A的临时数据
这些临时数据就是进程A的上下文数据!!!!!!!!
如果丢弃,那下次进程A再次被调度时,就不知道运行到哪里,以及函数的返回值都会丢失
进程运行期间,被抢占了、时间片到了、主动出让了,导致进程B被切换上去,当进程A被切走时,进程A要带走自己的上下文数据 ------ 带走保存的目的:下一次被调度时,能恢复上去,就能继续按照之前的逻辑继续向后运行
环境变量
os本身,也可以自己定义一些环境变量,这些变量通常是与系统、用户相关的一些系统参数。
PATH环境变量
系统命令可以直接运行,我自己的程序必须带路径才能运行
系统要运行一条命令,就要先找到它。系统命令是可被找到的,因为这些命令的路径在环境变量PATH中
PATH里保存了大量的路径,如果运行程序时不指定路径,就会去这些搜索路径中找
例如:执行ls时,系统先在PATH里的地址中查找ls.
添加路径到PATH(在命令行上改环境变量,只能在本次登录中被修改,退出后会重置)
把可执行程序拷贝到PATH维护的路径中,这就是安装程序。
常见环境变量
PATH:指定可执行程序的搜索路径(不手动指定可执行程序的路径时,就会到这些路径里去找)
HOME:指定当前用户的主工作目录(/home/xxx),root是/root,不同用户的HOME是不同的
SHELL:当前Shell,为/bin/bash
每个环境变量保存的内容没有关联,查看linux下所有的环境变量 ------ env命令
代码中获取环境变量
1 main函数获取
arduino
int main(int argc,char* argv[],char* env[])
{
for(int i=0;env[i]!= NULL;++i)
{
printf("%s\n",env[i]);
}
return 0;
}
main函数的形参都会有一个环境变量表,是一个字符指针数组,每个指针指向一个字符串
2 unistd.h中定义了一个全局变量environ,和main函数里的char* env[]没有区别
用man手册,在命令行中输入man environ就可以找到:
3 获取某一个环境变量 ------char* getenv(const char* name)
perl
printf("%s\n",getenv("PATH"));
进程环境变量来源
子进程的环境变量是从父进程那继承的!!!
默认,所有的环境变量都会在父进程继承
arduino
int main(int argc,char* argv[],char* env[])
{
printf("%s\n",getenv("aaaaaa"));
return 0;
}
bash的环境变量从哪里来呢?
linux的配置文件,当登录的时候会自动执行,使用特定的命令给bash装载特定的环境变量
环境变量的全局属性
环境变量具有全局属性 ------ 可以被所有子进程继承
命令行也可以定义普通变量,但不能被子进程拿到
进程地址空间
排布
c/c++定义的所有变量,都要遵守以下规则:
栈区向地址减小的方向增长,堆栈相对而生
static修饰局部变量,本质将该变量,开辟在全局区域
静态变量存在全局区,字面常量在代码段
c
int un_g_val;
int g_val = 1;
int main(int argc,char* argv[],char* env[])
{
int a = 0;
int b = 0;
static int c = 1;
char* heap = (char*)malloc(sizeof(char));
char* heap1 =(char*)malloc(sizeof(char));
char* heap2 =(char*)malloc(sizeof(char));
printf("env:%p\n",env[0]);
printf("argv:%p\n",argv[0]);
printf("stack:---\n");
printf("&a=%p\n",&a);
printf("&b=%p\n",&b);
printf("&heap=%p\n",&heap);
printf("heap:%p\n",heap);
printf("heap1:%p\n",heap1);
printf("heap2:%p\n",heap2);
printf("un_g_val:%p\n",&un_g_val);
printf("g_val:%p\n",&g_val);
printf("&c(static):%p\n",&c);
printf("main:%p\n",&main);
printf("read only string addr:%p","aaa");
return 0;
}
这里的地址空间是内存吗
不是内存,甚至指针,都不是真正的内存地址!!!
perl
int g_val = 1;
int main(int argc,char* argv[],char* env[])
{
pid_t id = fork();
if(id == 0)
{
printf("child:g_val->10\n");
g_val = 10;
while(1)
{
sleep(1);
//子进程
printf("child:g_val = %d,&g_val=%p\n",g_val,&g_val);
}
}
else
{
while(1)
{
//父进程
sleep(1);
printf("parent:g_val = %d,&g_val=%p\n",g_val,&g_val);
}
}
return 0;
}
同一个地址,不同的进程读取时,出现了不同的值
这里的地址,绝对不是物理内存的地址!!!
如何理解进程地址空间
1 历史:直接访问物理内存
进程1、进程2、进程3在内存中运行,一旦发生野指针问题,例如进程1里非法访问/篡改了进程2的数据,
这会导致进程之间不具有独立性,且不安全
内存本身是可以随时被读写的,没有权限、不可读等,它就是个硬件,只负责存和取
所以我们不能让用户直接使用物理地址
2 现代计算机,提出了下面方式
用户不直接使用物理内存
linux给每一个进程创建一个地址空间(虚拟地址空间),这个空间上所有的地址,叫虚拟地址。
进程控制块可以找到虚拟地址空间,上面所用到的地址都是虚拟地址。
系统也一定存在一种映射机制,核心工作:把用户的代码通过该映射机制映射到物理内存中
凡是要访问物理内存,需要先进行映射!!!!!!!!!
(1) cpu拿到进程pcb中指向的代码和数据(或者上下文数据),cpu读到了某个虚拟地址
(2) 根据虚拟地址,经过映射,访问物理内存中的代码和数据
虚拟地址空间和映射机制的存在,可以在软件层上甄别为正常访问还是非法访问
如果是非法访问就禁止映射,变相保护了物理内存
进程地址空间的本质
每个进程都有进程地址空间,因此系统中会有许多进程地址空间,需要被管理起来
先描述,再组织
内核中的地址空间,本质也一定是一种结构体
同时,将来也一定要和特定的进程pcb关联起来!!!!
在进程的task_struct里,有一个指向进程地址空间的指针
区域划分
针对一个特定的区域,定义上start和end.
区域划分,本质是在一个范围里定义出start和end
地址空间是一种内核数据结构,它里面至少要有:各个区域的划分
arduino
struct mm_struct
{
int code_start;
int code_end;
int init_start;
int init_end;
int uninit_start;
int uninit_end;
int heap_start;
int heap_end;
..............................
}
范围变化本质就是对start或end标记值 +-特定的范围即可
内核中是用无符号整型来表示各个区域的
页表
由操作系统维护(os内的一种内核数据结构),用来虚拟地址到物理地址之间的映射
地址空间和页表,是每个进程都私有一份
只要保证每个进程的页表映射的是物理内存的不同区域,就能做到进程之间不会互相干扰,保证进程的独立性
fork之后,为什么一个变量保存两个不同的值
在创建子进程后,父子进程的g_val相同,页表映射到同一位置
当子进程改了g_val值时,os在内存的其他地方拷贝一份g_val,让子进程的页表映射到这个位置,再进行修改,虚拟地址的值不受影响,但通过页表映射到不同位置
父子进程各自在物理内存中,有属于自己的变量空间!!
只是在用户层用同一个变量(虚拟地址)来标识了。
编译器对虚拟地址空间的支持
可执行程序在磁盘上,内部已经有虚拟地址了
objdump是用来显示二进制文件信息的工具
objdump -h 可执行程序:可以看到VMA(Virtual Memory Address)
h选项:显示文件的节信息,可执行程序是由节构成的,每个节包含特定代码和数据。
例如:.text存放可执行程序的代码(所有函数和指令),.data存放初始化的全局变量和静态变量
地址空间不仅是os内部要遵守的编译器也要遵守!!!编译器编译代码时,就已经给我们形成了各个区域的代码区、数据区......并且采用和linux内核一样的编址方式,给每一个变量,每一行代码都进行了编址。
即:程序在编译时每一个变量/函数/指令早已具有了一个虚拟地址。 加载程序时,其实虚拟地址也被加载进去了
ini
int a = 10; //0x1
int b = 100; //0x2
int c = 1000;//0x3
进程内部的地址,用的是编译器编译好的虚拟地址
当程序加载到内存时,每行代码、每个变量才具有了一个物理地址
1 地址空间可以按照加载进来的程序代码和数据,来限定地址空间的区域划分,填充mm_struct
2 把虚拟地址填写到页表左侧,再把实际代码和数据的物理地址,填写到页表右侧,此时就构建好了映射关系。
3 cpu读取时,先根据进程pcb获取进程下一行代码或数据的虚拟地址,再根据页表,从内存中拿到代码和数据
4 当cpu读到指令时,指令内部也有地址,指令内部的地址,还是虚拟地址
cpu至始至终都在用虚拟地址,通过页表映射拿到内存中的值
为什么要存在地址空间和页表
1 因为有地址空间和页表的存在,可以对用户的非法访问进行有效拦截
凡是非法的访问或映射,os都会识别到,并终止你这个进程
ini
char* str = "dadwadaw";
str[0] = 'A';//只读
2 实现了内存管理和进程管理的解耦
地址空间的排布是严格的,而页表可以帮我们映射到物理内存的任何位置,所以在物理内存中,可以对未来的数据进行任意位置的加载,只要保证页表映射即可
3 在分配内存时,可以采用延迟分配的策略,提高内存的有效使用率
如果我申请了空间,不立刻使用,就会造成空间的浪费。因为有地址空间的存在,上层申请空间,其实是在地址空间上申请的,物理内存可以甚至1byte都不给你!!!
当你真正对物理地址空间访问时(os自动完成,用户包括进程零感知),才执行内存的相关管理算法,帮你申请内存,构建页表映射关系,然后,再让你进行内存的访问。
4 在进程视角(用户视角),所有的内存分布,都可以是有序的,方便确定越界情况
5 进程的独立性,可以通过地址空间+页表的方式实现
因为有地址空间的存在,这些虚拟地址可以通过页表映射到不同的区域,来实现进程的独立性
拓展
实际上各自外设,例如磁盘、显卡等,都是有各种寄存器的.
把内存的存储空间,和外设的寄存器,统一编址,当作内存来看,这样计算机在写数据时,写入外设就好像直接写到内存里,只有时间上的差别。但是每一种硬件本身是不同的,所以要引入一个虚拟地址空间。
虚拟地址空间统一将不同硬件对应的设备(内存、网卡、显卡、磁盘等)进行编址,从0000......00到fff......fff,我们访问某些硬件时,特定的硬件和地址空间有对应的关系。所以在访问某个区域时,如果对应的是内存就访问内存,如果外设就访问外设,
在进程这一端看来,它是以统一的视角去看待所有设备
写时拷贝
引出
fork创建子进程,os做了什么?
系统里多了一个进程:进程 = 内核数据结构+进程代码和数据
1 为子进程创建内核数据结构
2 将父进程部分数据结构内容拷贝给子进程,例如上下文数据、进程地址空间的区域划分、页表的映射
3 添加子进程到运行队列中
但是,子进程也要有自己的代码和数据。一般而言,子进程没有自己的代码和数据!
所以子进程只能使用 父进程的代码和数据
代码:只读的,父子共享,没有问题
之前的代码,子进程也能看到,但子进程不会执行
因为进程随时可能被中断(可能并没有执行完),下次回来,还必须从之前的位置继续运行(不是最开始),就要求cpu必须随时记录下,当前进程执行的位置.所以cpu内有对应的寄存器数据,用来记录当前进程的执行位置EIP-pc指针
fork创建子进程,子进程会认为自己的EIP起始值,是fork之后的代码位置
数据:可能被修改,写时拷贝
是什么
本质是一种延迟申请,修改/写入的时候再申请内存,拷贝数据。
用的时候再分配,是高效使用内存的一种表现
作用
父子进程得以彻底分离,代码共享,数据写时拷贝,保证进程独立性
进程终止
进程终止时,os做了什么?
进程 = 内核数据结构 + 进程对应的代码和数据
释放进程申请的相关数据结构,以及对应代码和数据
进程终止的情况
a 代码跑完,结果正确
b 代码跑完,结果不正确
c 代码没有跑完,进程异常终止,此时结果不重要了
(1) main函数的返回值?意义是什么?
是进程的退出码,用来表示进程的结果是否正确
0:代码跑完,结果success
非0:代码跑完,结果不正确,可能有很多错误原因
csharp
int main(){
return 2;//返回给父进程,用来评判该进程执行结果用的,可以忽略
}
在命令行中,获取最近一个进程执行完毕之后,它的退出码 ------ echo $?
echo $?本身也是一个进程
(2) char* strerror(int errnum);//将退出码转换成字符串描述
查看常见的错误原因(系统默认给的退出码都代表什么错误原因)
perl
for(int i = 0; i < 128; ++i)
{
printf("%d: %s\n",i,strerror(i));
}
(3) 也能自己设计一套退出方案,方便定位错误
(4) 代码没跑完,程序崩溃,退出码无意义。例如:野指针问题、除0错误
在代码中终止进程的方式
(1) main函数里的return语句
(2) 代码的任意位置终止进程:
void exit(int status); // c语言提供的库函数 stdlib.h
void _exit(int status);//系统调用接口
区别:exit会刷新缓冲区到os,_exit不会
scss
printf("you can see me");
//数据在缓冲区内
exit(0);//会将缓冲区内的数据全部刷新出来
//_exit(0);//不会刷新缓冲区的数据
exit底层调用了_exit,只是在调用之前,要刷新缓冲区到os内
调用printf等io函数,数据要保存到缓冲区,这个缓冲区,是c标准库函数维护的
进程等待
为什么要进行进程等待
子进程退出,父进程不管子进程,子进程就要处于僵尸状态 ------ 导致内存泄漏
父进程创建了子进程,是要子进程办事的,那么子进程把任务完成得怎么样?父进程需要关心吗?如果需要,如何得知?如果不需要,该如何处理?
核心任务:回收子进程相关内存资源;父进程通过等待,获得子进程退出结果
进程等待基本使用
pid_t wait(int* status)
成功返回子进程pid,失败返回-1 ------ 阻塞式等待任意子进程退出
scss
pid_t id = fork();
if (id < 0)
{
perror("fork");
exit(1);
}
else if (id == 0)
{
// 子进程
sleep(5);
exit(0);
}
else
{
// 父进程
pid_t ret = wait(NULL);//wait是阻塞式等待,等待子进程退出
if(ret > 0)
{
printf("等待子进程成功,ret=%d\n",ret);
}
}
return 0;
pid_t waitpid(pid_t pid, int* status,int options);
(1) pid_t pid:指定要等哪一个子进程 ------ 一个父进程可能有多个子进程
pid = -1:等待任意一个子进程退出,与wait等效
(2) int options:0表示阻塞等待;WNOHANG表示非阻塞等待
(3) int* status:输出型参数,获得子进程得退出结果,NULL表示不关系结果
arduino
pid_t ret = waitpid(-1,NULL,0);//等价于wait(NULL)
status的构成
子进程的退出结果有3种:
(1) 代码跑完,结果正确
(2) 代码跑完,结果不正确
(3) 代码没有跑完,进程崩溃了 ------ 例如野指针越界访问、除0错误
本质是os通过发送信号,杀掉了这个进程
status按照比特位的方式,将32个比特位进行划分 ------ 只关注低16位
0x7f -> 0111 1111,子进程收到的信号 = status & 0x7f,如果为0表示正常退出
0xff -> 1111 1111,子进程的退出码 = (status >> 8) & 0xff
系统提供宏不需要手动位操作
WIFEXITED(status): 查看进程是否正常退出
WEXITSTATUS(status): 查看进程的退出码
scss
pid_t id = fork();
if (id == 0)
{
// 子进程
sleep(5);
exit(99);
}
else if(id > 0)
{
// 父进程
int status = 0;
pid_t ret = waitpid(-1,&status,0);//等价wait(NULL)
if(ret > 0)
{
printf("等待子进程成功,子进程pid=%d,子进程收到的信号signal=%d,退出码exitCode=%d\n",ret,status&0x7f,(status>>8)&0xFF);
if(WIFEXITED(status) == 1)
{
//子进程正常退出
printf("子进程正常退出,子进程的退出码是%d\n",WEXITSTATUS(status));
}
}
}
wait/waitpid原理
进程具有独立性,进程退出码,不也是子进程的数据吗?父进程怎么通过wait/waitpid拿到???
僵尸进程:至少要保留进程的pcb,task_struct里面保留了任何进程退出时的退出结果,让父进程调用wait/waitpid读取。
wait/waitpid本质是在读取子进程的task_struct结构体里的退出码和退出信号,将它们放进status里
waitpid伪代码实现
ini
waitpid(child_id,status,flag){
//os内核实现
//检测子进程退出状态,查看子进程的task_struct里的内核status
if(内核status == 退出){
//读取子进程task_struct里的退出结果,放入*status里
*status = 0;
*status |= sig_number;
*status |= (exit_code << 8);
//返回子进程pid
return child_pid;
}
else if(内核status == 没退出)
{
if(flag == 0)
{
//把父进程pcb放到等待队列中,等待子进程退出
阻塞;//进程阻塞,本质是进程阻塞在系统函数的内部!!!!
//当条件满足时,父进程被唤醒,继续往后执行
//读取子进程task_struct里的退出结果,放入*status里
*status = 0;
*status |= sig_number;
*status |= (exit_code << 8);
//返回子进程pid
return child_pid;
}
else if(flag == WNOHANG)
{
//不阻塞进程
return 0;
}
}
else{//出错了 return -1;}
}
基于非阻塞调用的轮询检测方案
父进程通过调用waitpid来等待,如果父进程发现子进程没有退出,waitpid直接返回,不会阻塞
scss
typedef void (*callback)();//函数指针类型
std::vector<callback> callbacks;
void task1()
{
printf("任务1\n");
}
void task2()
{
printf("任务2\n");
}
//想要父进程闲的时候执行的方法
void load()
{
callbacks.push_back(task1);
callbacks.push_back(task2);
}
int main()
{
pid_t id = fork();
if (id == 0)
{
// 子进程
sleep(3);
exit(99);
}
else if(id > 0)
{
//父进程
int status = 0;
int quit = 0;
while(!quit)
{
pid_t ret = waitpid(id,&status,WNOHANG);
if(ret > 0){
//等待成功,子进程退出
quit = 1;
if(WIFEXITED(status) == 1){
printf("子进程pid=%d,子进程退出码exitCode=%d\n",ret,WEXITSTATUS(status));
}
else{
printf("子进程收到信号异常退出:%d\n",status&0x7f);
}
}
else if(ret == 0)
{
//子进程还没有退出,再干会别的事情
printf("子进程还在运行\n");
if(callbacks.empty() == true)
load();
for(auto func:callbacks)
{
func();
}
sleep(1);
}
}
}
return 0;
}
进程程序替换
是什么
fork之后,父子各自执行父进程代码的一部分,但如果子进程想执行一个全新的程序呢?
进程程序替换通过特定接口,加载磁盘上的全新程序(代码和数据),加载到调用进程的地址空间中
让子进程执行其他程序
原理
将磁盘上的程序加载到内存,并和当前进程的页表,重新建立映射。
进程替换,没有创建新的子进程!!!
当子进程调用程序替换函数时,也是一种"写入",代码也要写时拷贝,让父子进程分离。
基本使用
int execl(const char* path,const char* arg,...);
path:程序所在路径+程序名
arg:可变参数列表 -> 可以传入不定个数的参数。命令行上程序怎么执行,这里的参数就怎么传
最后一个参数必须是NULL,表示传递完毕
调用失败,返回-1;调用成功,没有返回值
例如:让程序执行ls -a
arduino
int main()
{
printf("当前进程的开始\n");
//程序替换完成后,会将当前进程所有代码和数据都进行替换,包括已经执行和没有执行的
execl("/usr/bin/ls","ls","-a",NULL); //后续代码不会被执行
printf("当前进程的结束\n");
return 0;
}
int execv(const char* path,char* const argv[]);
char* const argv[]:指针数组,里面保存一个个字符串;将命令行参数放进该数组
arduino
int main()
{
pid_t id = fork();
if (id == 0)
{
char* const argv[] = {
"ls",
"-a",
"-l",
"-i",
NULL //注意还是以NULL结尾
};
//子进程
execv("/usr/bin/ls",argv); //与execl只有传参方式的区别
exit(1);
}
else{
//父进程
int status = 0;
pid_t ret = waitpid(id,&status,0);//阻塞等待
if(ret > 0){
//等待成功
printf("wait success,exit code:%d,%d\n",WEXITSTATUS(status),(status >> 8)&0xff);
}
}
return 0;
}
int execlp(const char* file,const char* arg,...);
要执行一个程序,必须先通过路径找到。
没有指定路径,在环境变量PATH中查找
arduino
//第一个"ls"代表:你要执行谁
//后面的参数代表:你想怎么执行
execlp("ls","ls","-l",NULL);
int execle(const char* path,const char* arg,...,char* const envp[]);
char* const envp[]:给替换的程序传递环境变量,传递后会覆盖之前子进程本身的环境变量
arduino
//newproc.cc
#include <iostream>
using namespace std;
int main(int argc,char* argv[],char* env[])
{
for(int i = 0; env[i] != nullptr;++i)
{
cout << env[i] << endl;
}
return 0;
}
scss
//myproc.cc
pid_t id = fork();
if (id == 0)
{
char* _env[]={
"MYPROCCC=123",
NULL
};
//子进程
execle("./newproc","newproc",NULL,_env);
exit(1);
}
小结
exec*:是加载器的底层接口
l:理解为list,后面传命令行参数时,把参数一个一个往后传
v:理解为vector,把参数放到一个数组里传
p:理解为PATH,只需传递程序名,会自动在PATH环境变量的路径中找这个程序
e:理解为环境变量,程序替换后,传递新的环境变量,覆盖进程原来的环境变量
程序替换的系统调用:
以上的接口只是系统调用的封装