进程概念
- 进程是程序的一个执行实例
- 进程是系统资源分配(CPU时间、内存)的基本单位
- 进程ID可以唯一标识一个进程(进程ID为非负整形数据)
- 进程信息被放在一个叫做进程控制块(进程属性集合)的数据结构中—PCB
- Linux环境下的PCB是task_struct
- task_struct内容分类:
struct task_struct
{
//进程描述符:描述本进程的唯一标识符,用来区分其他进程
//状态:任务状态、退出代码、退出信号等
//优先级:相对于其他进程的优先级
//程序计数器:内存中即将被执行的下一条指令
//内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
//上下文数据:进程执行时处理器的寄存器中的数据
//I/O状态信息:包括显示的I/O请求、分配给进程的I/O设备和被进程使用的文件列表
//记账信息:可能包括处理器时间总和,使用的时钟数总和、时间限制、记账号等
//其他信息
};
关于task_struct更多信息,大家可以参考:http://www.cnblogs.com/tongyan2/p/5544887.html
创建进程
首先在内存中为新进程创建一个task_struct结构,然后将父进程的task_struct内容复制到子进程的task_struct中,再修改部分数据。然后为子进程分配新的内核堆栈、新的PID、再将task_struct这个node添加到链表中。所谓创建,实际上是“clone”。
fork()
#include<stdio.h>
pid_t fork(void)
//返回值:子进程返回0,父进程返回子进程ID。出错返回-1
Q:为什么父进程返回值为子进程ID,而子进程返回值为0?
A1:将子进程ID返回给父进程的原因是:父进程的返回值我们所能考虑到的就是父进程本身的ID和它所创建的子进程的ID,因为一个父进程可以有多个子进程,并且没有一个函数可以同时返回所有子进程的进程ID;所以每创建一个子进程,父进程都会返回其子进程的进程ID
A2:子进程返回为0的原因是:子进程的返回值我们所能考虑到的就是子进程本身的ID和父进程的ID,但是因为进程ID为0的进程为系统进程,所以子进程ID不可能为0;而父进程的ID我们可以通过函数调用getppid()来获得,所以也不需要子进程来返回
进程创建实例:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(void)
{
pid_t pid;
int i=0;
for(i=0;i<3;i++)
{
pid=fork();
if(pid==0)
{
printf("i am child:%d pid=%d\n",getpid(),pid);
break; //一个父进程创建多个子进程的时候避免由已创建的子进程再去创建子进程
}
else if(pid<0)
perror("fork()"),exit(1);
else
printf("i am father:%d pid=%d\n",getppid(),pid);
sleep(1);
}
return 0;
}
运行结果如下,一个父进创建多个子进程,父进程每次返回新创建的子进程的ID:
i am father:13254 pid=13996 i am child:13996 pid=0 i am father:13254 pid=13997 i am child:13997 pid=0 i am father:13254 pid=13998 i am child:13998 pid=0
- 从上述实例运行结果来看:fork()创建的子进程与父进程是交替运行的
- 事实上,在调用fork()之后,子进程和父进程的执行顺序是不太确定的,这是由内核的进程调度算法决定
注意:调用fork()函数之后,一定是两个进程同时执行fork()函数之后的代码,而之前的代码已经由父进程执行完毕
既然子进程是由父进程所创建的,那么子进程和父进程有什么联系呢?(代码、数据、空间)
- 通过对数据进行操作,观察父子进程之间的联系。实例:
#include<stdio.h>
#include<unistd.h>
int main(void)
{
int var=10;
pid_t pid;
pid=fork();
while(1)
{
if(pid<0)
{
perror("fork");
return 1;
}
else if(pid==0)
{
var+=2;
printf("i am child:%d pid:%d var=%d(%p)\n",getpid(),pid,var,&var);
}
else
{
var-=2;
printf("i am father:%d pid:%d var=%d(%p)\n",getpid(),pid,var,&var);
}
sleep(1);
}
}
程序运行结果如下所示:
i am father:6549 pid:6550 var=8(0xbff59398)
i am child:6550 pid:0 var=12(0xbff59398)
i am father:6549 pid:6550 var=6(0xbff59398)
i am child:6550 pid:0 var=14(0xbff59398)
i am father:6549 pid:6550 var=4(0xbff59398)
i am child:6550 pid:0 var=16(0xbff59398)
i am father:6549 pid:6550 var=2(0xbff59398)
i am child:6550 pid:0 var=18(0xbff59398)
- 子进程和父进程对各自的变量没有任何影响
- 父子进程var的地址是相同的
- 因为子进程是父进程的副本,所以它拥有子进程数据空间、栈、堆的副本。
fork写时复制
在Linux程序中,fork()会产生一个和父进程完全相同的子进程;但fork之后经常跟随着exec系统调用,所以现在的很多实现并不执行一个父进程数据段、堆、栈的完全副本,作为替代,linux中引入了“写时复制“技术,这些区域父子进程共享,内核将其权限改为只读,若是父子进程中的任一个想要修改这些区域,则内核只为要修改的那一区域制作副本;也就是只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。
在fork()后,exec前两个父子进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,也就是说父子进程虽然虚拟空间不同,但是都指向同一内存区域。当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。
如果没有exec,内核会给子进程的数据段、堆栈段分配相应的物理空间(这样两者有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(两者的代码完全相同)。但是有了exec,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间。
fork之后内核会通过将子进程放在队列的前面,以让子进程先执行,以免父进程执行导致写时复制,而后子进程执行exec系统调用,因无意义的复制而造成效率的下降。
fork()有以下两种用法:
- 一个父进程希望复制自己,使父进程和子进程同时执行不同的代码段
- 一个进程要执行一个不同的程序
vfork()
vfork()函数的调用序列与返回值与fork相同,但语义不同
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
进程创建实例:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
int main(void)
{
pid_t pid;
int var=10;
pid=vfork();
printf("i am father(B):%d pid=%d var=%d(%p)\n",getpid(),pid,var,&var);
while(1)
{
if(pid<0)
{
perror("vfork");
return 1;
}
else if(pid==0)
{
var+=2;
printf("i am child:%d pid=%d var=%d(%p)\n",getpid(),pid,var,&var);
exit(0); //子进程不退出,父进程无法执行
}
else
{ var-=2;
printf("i am father(A):%d pid=%d var=%d(%p)\n",getpid(),pid,var,&var);
exit(0);
}
}
return 0;
}
i am father(B):6592 pid=0 var=10(0xbf85b7a8) i am child:6592 pid=0 var=12(0xbf85b7a8) i am father(A):6591 pid=6592 var=12(0xbf85b7a8) //father(A)和father(B)分别表示在vfork函数调用前、后父进程的状态
- 通过程序实例结果观察:若子进程不退出,父进程则无法执行;vfork()函数保证子进程先运行,在调用exit后父进程才有可能被调度运行
- 子进程对变量做+2的操作,结果父进程也跟着进行了+2的操作,因为子进程在父进程的地址空间中运行
总结:内核是如何为父子进程分配空间的
父进程f1其虚拟地址空间上有代码段、数据段、堆和栈四个区域,内核会为其分配相应的物理内存
f1创建子进程f2,为其复制代码段、数据段、堆和栈四部分;为其分配物理内存:f2的代码段->f1代码段的物理地址;f2的数据段->f2自己的数据段块;f2的堆和栈->f2自己的堆栈块
写时拷贝:内核只为新生成的子进程创建虚拟空间结构,它们来复制于父进程的虚拟空间结构,但是不为这些段分配物理内存,它们共享父进程的物理空间,当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。
vfork():内核不为子进程创建虚拟内存空间,直接与父进程共享同一块虚拟内存空间,相应的也共享同一块物理内存
进程终止
进程终止的五种状态:
- 正常
- main 退出
- exit 刷新输出缓冲区,相当于C++调析构函数
- _exit(系统调用函数)
- 不正常
- ctrl+c
- abort()
- kill
- 正常退出,退出码为0,不正常退出,退出码非0
- 退出码:0-255 只用了int的8个bit位,其余的位有别的用途,比如:程序是否正常退出。如果是异常退出,到底是什么原因导致退出的。
进程状态
- 创建:进程在创建时需要申请一个空白PCB,向其中填写控制和管理进程的信息,完成资源分配。如果创建工作无法完成,比如资源无法满足,就无法被调度运行,把此时进程所处状态称为创建状态
- 就绪:进程已经准备好,已分配到所需资源,只要分配到CPU就能够立即运行;进程时间片用完也会进入就绪状态
- 阻塞:正在执行的进程由于某些事件(I/O请求,申请缓存区失败)而暂时无法运行,进程受到阻塞。在满足请求时进入就绪状态等待系统调用
- 运行:进程处于就绪状态被调度后,进程进入执行状态
- 终止:进程结束,或出现错误,或被系统终止,进入终止状态。无法再执行
僵尸进程
- 僵尸进程:当子进程比父进程先结束,而父进程又没有回收子进程且没有释放子进程占用的资源,此时子进程将成为一个僵尸进程。如果父进程先退出 ,子进程被init接管,子进程退出后init会回收其占用的相关资源
- 僵尸进程的危害:在Unix系统管理中,当用ps命令观察进程的执行状态时,经常看到某些进程的状态栏为defunct,这就是所谓的“僵尸”进程。“僵尸”进程是一个早已死亡的进程,但在进程表(processs table)中仍占了一个位置(slot)。由于进程表的容量是有限的,所以,defunct进程不仅占用系统的内存资源,影响系统的性能,而且如果其数目太多,还会导致系统瘫痪
下面给大家展示一个僵尸进程的实例:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(void)
{
pid_t pid;
pid=fork();
if(pid<0)
{
perror("fork");
exit(0);
}
else if(pid>0)
{
printf("parent[%d] is sleeping...\n",getpid());
sleep(30);
}
else
{
printf("chile[%d] is begin Z...\n",getpid());
sleep(5);
exit(EXIT_SUCCESS);
}
return 0;
}
程序运行结果如下所示:
parent[11328] is sleeping...
chile[11329] is begin Z...
进程监控命令行脚本: while :; do ps aux | grep a.out | grep -v grep;sleep 1;echo “####################”; done
编译并开启另一个终端,输入上述进程监控命令,就可以看到进程状态了,如下所示:
root 11306 0.0 0.0 1868 364 pts/0 S+ 05:55 0:00 ./a.out
root 11307 0.0 0.0 1868 228 pts/0 S+ 05:55 0:00 ./a.out
####################
root 11306 0.0 0.0 1868 364 pts/0 S+ 05:55 0:00 ./a.out
root 11307 0.0 0.0 1868 228 pts/0 S+ 05:55 0:00 ./a.out
####################
root 11306 0.0 0.0 1868 364 pts/0 S+ 05:55 0:00 ./a.out
root 11307 0.0 0.0 0 0 pts/0 Z+ 05:55 0:00 [a.out] <defunct>
清除僵尸进程:
- 改写父进程,在子进程死后要为它收尸
- 把父进程杀掉。父进程死后,僵尸进程成为”孤儿进程”,过继给1号进程init,init始终会负责清理僵尸进程.它产生的所有僵尸进程也跟着消失
孤儿进程
- 一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。孤儿进程并不会有什么危害
- 孤儿进程创建实例
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(void)
{
pid_t pid;
pid=fork();
if(pid<0)
perror("fork"),exit(0);
else if(pid==0)
{
printf("i am child:%d pid=%d\n",getpid(),pid);
sleep(10);
}
else
{
printf("i am father:%d pid=%d\n",getpid(),pid);
sleep(3);
exit(0);
}
return 0;
}
运行结果:
i am father:11386 pid=11387 i am child:11387 pid=0
检测结果:
root 11386 0.0 0.0 1868 368 pts/0 S+ 06:20 0:00 ./a.out
root 11387 0.0 0.0 1868 232 pts/0 S+ 06:20 0:00 ./a.out
####################
root 11387 0.0 0.0 1868 232 pts/0 S 06:20 0:00 ./a.out
####################
root 11387 0.0 0.0 1868 232 pts/0 S 06:20 0:00 ./a.out