目录
进程与程序
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,所以输出是乱序的。
非阻塞回收比阻塞回收版本有更高的自由度,可以执行自身代码段。
无论是阻塞回收还是非阻塞回收都称为主动回收,这并不合理在子进程未结束前父进程产生大量回收开销,后序讲解被动回收方案。