目录
进程与程序
1.1进程是什么
1.2程序,进程之间的关系
1.3进程的生存环境
1.4进程的状态转换
1.5关于内核层与用户层
1.6保存和恢复处理器现场
进程原语
2.1fork()
编辑
2.1.1父子进程的继承
2.1.2父子进程共享fork()栈帧
2.1.3打印进程id和父进程id
2.1.4循环创建进程
2.1.5fork()版本
2.2execl()
2.3wait()
2.4waitpid()
进程与程序
1.1进程是什么
进程是一个任务执行单元(逻辑单元)。
1.2程序,进程之间的关系
程序是静态数据(静态表现)存储在磁盘中。
进程是逻辑执行单位在cpu,内存,磁盘帮助用户使用系统资源完成特定任务。
程序是进程的静态表现,进程是程序的动态表现。
创建一个进程
生成可执行程序
我们发现把可执行程序删除后,进程依旧进行。
如果进程执行时,没有回访文件数据,那么这些文件数据都可以删除,程序文件与进程是没有必然联系的。
1.3进程的生存环境
内核层(系统层)->PCB(进程控制块)pid-----------------------------(概括一下就是:存储进程信息)
用户层->命令行参数(int argc char** argv),环境变量(系统默认),栈(0xc3000),库,堆,DATA(已初始化的全局静态变量),BSS(未初始化的全局数据),Text 代码段------------------------(概括一下就是:存储进程业务逻辑和任务数据)->映射机制,虚拟内存映射到物理内存,内存管理器有一个虚拟内存映射表。
x32位系统用户层和内核层有两种分配方式,0~3G 3~4G,0~2G 2G~4G,32位系统可用3级指针寻址。
x64位系统0~8T 8~16T,64位·系统是四级间接寻页。
每个进程通过映射方式访问物理内存,可以后序通过交换机制将闲置内存给别人使用,最大化利用内存,虚拟内存概念,可以避免用户直接访问物理内存。
每个进程执行时拥有独立的物理内存,进程资源独享。
内核空间映射内存是共享内存。用户空间映射内存是独立内存。
内存的基本单位是page,一般是4k。
内存的权限:PROT_READ只读,PROT_WRITE只写,PROT_EXEC执行,PROT_NONE都不行
1.4进程的状态转换
就绪态:进程准备就绪等待cpu资源而后开始运行。
终止态:进程结束退出,进程资源释放。
运行态:进程得到时间片开始执行。
阻塞态:放弃时间片,可以被强制中断EINTR,阻塞的进程资源在内存中。
挂起态:放弃时间片,无法被强制中断,挂起的进程资源被交换到外存中。
(僵尸态Linux下)
(孤儿态Linux下)
例子:当前进程a占用cpu,现在要把cpu释放交给进程b,怎么办?sleep(0);sleep释放cpu,进程有就近原则就是b,sleep让进程切换成阻塞态放弃cpu。
就绪态可以切换到运行态,运行态可以切换到就绪态。
挂起态可以切换到就绪态,阻塞态可以切换到就绪态。
进程在任意一种状态都能切换到终止态。
1.5关于内核层与用户层
电脑中所有设备都是cpu来访问控制的。
cpu有寄存器,运算器,控制器,译码器。
电脑中所有设备都是cpu来访问控制的,用户层执行以低权限使用cpu系统资源和设备,内核层执行以高权限使用cpu系统资源和设备。(系统调用就是cpu的权限切换)
1.6保存和恢复处理器现场
假设一个进程在执行指令的过程中耗尽时间片,那么会触发中断,保存处理器现场,内核栈->用于保存恢复处理器现场,这样保证再多的进程共享寄存器也不会引发异常。
进程原语
操作系统提供的一些列系统函数接口,让用户完成进程开发。
2.1fork()
父进程调用fork()创建一个子进程。
在Linux操作系统中几乎所有进程都有父亲。
根进程,刚开机就创建出来了。就根进程没父进程。根进程也称为init进程,系统的1号进程。
强亲缘关系:子进程从被创建直到进程结束,父进程全程参与。
如果是多级别的父子进程那么具备一定的亲缘关系(继承)。
fork();返回值类型是pid_t。
父进程从代码的起始位置执行到末尾,子进程从fork之后执行到末尾。
ps -ajx查看亲缘关系。
2.1.1父子进程的继承
在创建子进程之后,父进程将部分内容继承给子进程,子进程执行。
拷贝继承:
子进程可以访问父进程所有资源,无论是堆栈数据还是代码段。
父进程的资源数据完全拷贝给子进程,子进程任意访问修改与父进程无关。
2.1.2父子进程共享fork()栈帧
克隆完之后返回子进程id,父进程接收到然后返回。fork()它自己有个返回值,子进程来的时候最后执行了一个return 0。
因为printf
会将输出缓存到缓冲区中,所以有可能在子进程中的输出还没有刷新到屏幕上之前,两个进程都已经执行了后续的代码。所以先输出那个666了。
2.1.3打印进程id和父进程id
getpid()当前进程id,父进程id。
2.1.4循环创建进程
如果在循环中不加是子进程就退出,那会创建出个进程。
如图:
如果加上就正常了,不能让子进程也进入循环。
2.1.5fork()版本
第一版本:
采用拷贝继承,父进程拷贝资源是强制性的,如果子进程不需要父进程资源,那么这个拷贝开销毫无意义。
第二版本:
使用vfork函数,不会产生用户层拷贝开销,一定需要结合execl函数使用。
第三版本:
可以察觉子进程是否需要资源,读时共享,写时复制。
有一个映射内存,可以让子进程来访问父进程资源,如果子进程尝试访问修改映射内存数据,触发写复制机制,父进程将特定数据拷贝给子进程。写时复制,父子对映射内存数据写,都触发写时复制。父进程修改数据为了保证子进程不异常,子进程要复制之前的数据。
2.2execl()
进程功能重载。
重载可以复制其他进程的功能,无需对程序进行代码实现,重载即可。
which 命令名#寻址命令。
execl(命令的绝对路径,命令第一个参数,命令第二个参数,...,命令第n个参数,哨兵节点(NULL));
execl,预启动命令,重载目标用户层,但是目标程序没有被执行。重载成功后子进程代码被覆盖,最终程序是在重载程序中的代码段结束。如果子进程想要执行自己定义代码,要在fork之后execl之前完成。
重载ls -l
先找到绝对路径。
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
int main()
{
pid_t pid;
pid=fork();
if(pid>0)
{
printf("father pid success %d",getpid());
pid_t wpid = wait(NULL);
printf("wait pid success %d",wpid);
}
else if(pid==0)
{
printf("A\n");
execl("/usr/bin/ls","ls","-l",NULL);
printf("B\n");
exit(4);
}
else exit(0);
return 0;
}
execl可以便于功能拓展,使用极少的代价重载使用复杂功能。可以实现主程序不动的情况下,动态迭代,只需要让主程序定时重载即可。
代码:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
int main()
{
pid_t pid;
pid=fork();
if(pid>0)
{
while(wait(NULL)>0)
{
sleep(1);
pid=fork();
if(pid==0) goto biu;
}
}
else if(pid==0){
biu: execl("/home/zzj/doit","doit",NULL);
printf("dddd\n");
}
else exit(0);
return 0;
}
#include<stdio.h>
int main()
{
printf("hhhhhhh\n");
}
改了之后就这样。
2.3wait()
进程结束后需要对其进行回收,否则该进程会称为僵尸进程,引发内存泄漏。
当进程结束后,PCB未被释放,导致内存泄漏,这种现象被称为僵尸进程。。
Linux或Unix操作系统中,子进程只能由父进程回收。exit(0)调用操作系统中_EXIT(),用户空间内存被完全释放,也会释放部分内核空间,但是残留PCB导致内存泄漏,父进程负责回收释放子进程PCB,对子进程进行验尸操作,了解子进程的退出原因。
wait()函数返回值是pid_t。回收成功返回僵尸进程的pid。没有子进程尝试回收,wait立即失败返回。wait函数是阻塞回收函数,等待子进程结束后立即回收,它可以回收任意一个子进程,每次回收一个僵尸进程。验尸:pid_t zpid=wait(int * status);可以传出子进程退出原因,不验尸:pid_t zpid=wait(NULL);
让子进程变为僵尸进程,然后父进程回收
这里看见进程已经被回收了。
2.4waitpid()
高级回收函数。
pid_t zpid=waitpid(pid_t pid,int* status,WNOHANG);
返回值>0回收成功,返回值为僵尸的pid。返回时-1,表示回收失败,返回值0,表示当前子进程未结束,非阻塞返回0.。
pid_t pid; pid=-1(回收任意子进程),pid>0(指定回收一个子进程),pid=0(同组回收),pid<-1(跨组回收)。
非阻塞回收代码:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
int main()
{
pid_t pid;
int i;
for( i=0;i<5;i++)
{
pid=fork();
//printf("%d\n",pid);
if(pid==0)break;
}
if(pid>0)
{//printf("%d\n",pid);
printf("father pid %d\n",getpid());
pid_t wpid;
while((wpid=waitpid(-1,NULL,WNOHANG))!=-1)
{
if(wpid>0)
{
printf("waitpid success %d\n",wpid);
}
else if(wpid==0){
printf("I am running\n");
usleep(500000);
}
}
}
else if(pid==0)
{
printf("son pid %d\n",getpid());
sleep(i);
exit(i);
}
else exit(0);
return 0;
}
结果,首先父进程创建子进程,上面提到过fork会给两个返回值,一个是给父进程的子进程id,一个是给子进程返回的0,返回0之后会立即执行else if那,而父进程等创建完最后一个29866>0,执行if>0,所以输出是乱序的。
非阻塞回收比阻塞回收版本有更高的自由度,可以执行自身代码段。
无论是阻塞回收还是非阻塞回收都称为主动回收,这并不合理在子进程未结束前父进程产生大量回收开销,后序讲解被动回收方案。