linux进程
进程的定义在这里就避开不谈,我们主要以进程模型为起点开始
0x1 进程的创建
操作系统需要有一种方式来创建进程。
4种主要事件会导致的创建:
- 系统初始化
OS启动时,会创建若干个进程,分为:前台进程 和 后台进程
停留在后台接收请求的进程叫守护进程 - 正在运行的程序执行了创建进程的系统调用
进程可以通过系统调用再创建一个新的进程 - 用户请求创建一个新进程
在交互式系统中,键入一个命令或点(双)击一个图标就可以启动一个程序,这也意味着进程的产生 - 一个批处理作业的初始化
大型机环境中,在用户提交批处理作业时,OS在有资源可用时会创建一个新的进程进行作业
从技术层面而言,上述情况中,新进程都是由一个已存在的进程执行了用于创建进程的系统调用而创建的。在UNIX中,创建新进程的系统调用有且只有一个:fork()。
调用fork()后,父子进程拥有相同的内存映像、环境字符串、打开文件。通常子进程会调用execve或者类似系统调用,修改其内存映像并运行一个新的程序。
在UNIX中,子进程的初始地址空间是父进程的一个副本,这涉及两个不同的地址空间,不可写的内存区是共享的。某些UNIX实现程序正文在两者之间共享,因为不能被修改。或者子进程共享父进程的所有内存,通过写时复制共享。总而言之,强调的点是:可写的内存不能被共享的。
0x2 fork()概览
fork()的作用是根据一个现有的进程复制出一个新进程,原来的进程称为父进程(Parent Process),新进程称为子进程(Child Process)
系统调用 | 描述 |
---|---|
fork | fork创造的子进程是父进程的完整副本,复制了父亲进程的资源,包括内存的内容task_struct内容 |
vfork | vfork创建的子进程与父进程共享数据段,而且由vfork()创建的子进程将先于父进程运行 |
clone | Linux上创建线程一般使用的是pthread库 实际上linux也给我们提供了创建线程的系统调用,就是clone |
fork()原型:
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
pid_t fpid; //fork() retern value
int count = 1;
fpid=fork();
if(fpid < 0)
{
printf("fork error : ");
}
else if(fpid == 0) // fork return 0 in the child process because child can get hid PID by getpid( )
{
printf("the child process, the count is: %d (%p),the pid is: %d\n", ++count, &count, getpid());
}
else // the PID of the child process is returned in the parent’s thread of execution
{
printf("the parent process, the count is: %d (%p),the pid is: %d\n", count, &count, getpid());
}
return EXIT_SUCCESS;
}
fork()调用信息:
包含头文件 <sys/types.h> 和 <unistd.h>
函数功能 : 创建一个子进程
函数原型 :pid_t fork(void);
参数 : 无参数。
返回值:
如果成功创建一个子进程,对于父进程来说返回子进程ID
如果成功创建一个子进程,对于子进程来说返回值为0
如果为-1表示创建失败
示意图如下:
fork()后父子进程形态
- 子进程继承父进程属性:
- uid, gid, euid, egid
- 附加组id(sgid, supplementary group id)
/* sgid引入原因是有时候希望这个用户属于多个其他部门,这些其他部门的gid就是sgid */ - 进程组id, 会话id
- SUID标记, SGID标记
- 控制终端
- 当前工作目录/根目录
- 文件创建时的umask
- 文件描述符的文件标志(close-on-exec)
- 信号屏蔽和处理
- 存储映射
- 资源限制
- 子进程独有(与父进程不同的地方):
- pid不同
- 进程时间被清空
- 文件锁没有继承
- 未处理信号被清空
- fork()后父子进程状态:
- fork系统调用之后,父子进程将交替执行
- 任何一个进程都必须有父进程
- 如果父进程先退出,子进程还没退出----那么子进程的父进程将变为init进程。
- 如果子进程先退出,父进程还没退出----那么子进程必须等到父进程捕获到了子进程的退出状态才真正结束,否则这个时候子进程就成为僵进程
- 子进程退出会发送SIGCHLD信号给父进程,可以选择忽略或使用信号处理函数接收处理就可以避免僵尸进程
- copy on write(写时复制)
定义:一个被使用在程式设计领域的最佳化策略。其基础的观念是,如果有多个呼叫者(callers)同时要求相同资源,他们会共同取得相同的指标指向相同的资源,直到某个呼叫者(caller)尝试修改资源时,系统才会真正复制一个副本(private copy)给该呼叫者,以避免被修改的资源被直接察觉到,这过程对其他的呼叫只都是通透的(transparently)。此作法主要的优点是如果呼叫者并没有修改该资源,就不会有副本(private copy)被建立。
解释:如果多个进程要读取它们自己的那部分资源的副本,那么复制是不必要的。每个进程只要保存一个指向这个资源的指针即可。如果一个进程要修改自己的那份资源的“副本”,那么就会复制那份资源。
fork就是基于写时复制,只读代码段是可以共享
若使用vfork 则子进程和父进程占用同一个内存映像,在子进程修改会影响父进程。 同时只有在子进程执行exec/exit之后才会运行父进程。实际上子进程占用的栈空间就是父进程的栈空间,所以需要非常小心。如果vfork的子进程并没有 exec或者是exit的话,那么子进程就会执行直到程序退出之后,父进程才开始执行。而这个时候父进程的内存已经完全被写坏。
0x3 fork()实战
现在来看一道linux开发的面试题
linux下gcc编译如下文件
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid1;
pid_t pid2;
pid1 = fork();
pid2 = fork();
printf("pid1:%d, pid2:%d\n", pid1, pid2);
已知从这个程序执行到这个程序的所有进程结束这个时间段内,没有其它新进程执行.问题如下:
- 请说出执行这个程序后,将一共运行几个进程?
- 如果其中一个进程的输出结果是“pid1:1001, pid2:1002”,写出其他进程的输出结果(不考虑进程执行顺序)
明显这道题的目的是考察linux下fork的执行机制。下面我们通过分析这个题目,谈谈linux下fork的运行机制。
预备知识
- 进程可以看做程序的一次执行过程。在linux下,每个进程有唯一的PID标识进程。PID是一个从1到32768的正整数,其中1一般是特殊进程init,其它进程从2开始依次编号。当用完32768后,从2重新开始
- linux中有一个叫进程表的结构用来存储当前正在运行的进程。可以使用
ps aux
命令查看所有正在运行的进程 - 进程在linux中呈树状结构,init为根节点,其它进程均有父进程,某进程的父进程就是启动这个进程的进程,这个进程叫做父进程的子进程
- fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等)数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程
解答
程序执行过程:
-
从shell中执行此程序,启动了一个进程。
我们设这个进程为P0,设其PID为XXX(解题过程不需知道其PID) -
当执行到pid1 = fork()时,P0启动一个子进程P1。
由题目知P1的PID为1001。我们暂且不管P1 -
P0中的fork返回1001给pid1
继续执行到pid2 = fork();此时启动另一个新进程,设为P2
由题目知P2的PID为1002。同样暂且不管P2 -
P0中的第二个fork返回1002给pid2
继续执行完后续程序,结束
所以,P0的结果为“pid1:1001, pid2:1002” -
再看P2,P2生成时,P0中pid1=1001,所以P2中pid1继承P0的1001
而作为子进程pid2=0,P2从第二个fork后开始执行,结束后输出**“pid1:1001, pid2:0”** -
接着看P1,P1中第一条fork返回0给pid1,接着执行
而后面接着的语句是pid2 = fork();执行到这里,P1又产生了一个新进程,设为P3,先不管P3 -
P1中第二条fork将P3的PID返回给pid2
由预备知识知P3的PID为1003,所以P1的pid2=1003
P1继续执行后续程序,结束,输出“pid1:0, pid2:1003” -
P3作为P1的子进程,继承P1中pid1=0,并且第二条fork将0返回给pid2
所以P3最后输出“pid1:0, pid2:0” -
至此,整个执行过程完毕
答案:
- 4个(P0, P1, P2, P3)
- pid1: 1001, pid2: 1002
pid1: 1001, pid2: 0
pid1: 0, pid2: 1003
pid1: 0, pid2: 0
DFS:
------->pid2 (forth)
|
|
|
------->pid1----------->pid1 (third)
| (1001)
|
|
| ------->pid2 (second)
| | (1002)
| |
| |
root----------->root----------->root (first)
以P0为root的进程数
验证:
下图即运行结果,注意以实际分配pid为基数计算即可(本例中是15638)