程 序原创作品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
计算机是一种精密的仪器,有一点错误都是不能成功执行的,计算机软件是靠一个个程序组成的,而程序又是一系列指令所组成。通过执行这样的一条条指令,计算机就能完成一个个任务了。
这里有两个概念比较容易混淆,平时我们在学习计算机的时候也是这样,那就是进程和程序的区别,这也是这篇博客的主要内容,进程。简单点讲,进程就是程序的动态执行,是一个运行起来的程序。
Linux其中一个重要的功能就是管理各种状态的进程,包括创建进程、管理进程、调度进程等一系列操作,正是由于有这些操作,才发挥了系统强大的能力。首先我们一起来熟悉一下进程。一般来说,进程都要经历3个状态,分别是就绪态、阻塞态、运行态。但是Linux操作系统将进程的状态进行了进一步划分,分为运行态、不可中断阻塞状态、可中断阻塞状态、挂起状态、僵尸状态5个状态。它们的具体含义是:
运行状态:当进程正在被CPU执行,或处于就绪状态,则称进程处于运行状态。
可中断睡眠状态:进程处于阻塞状态,系统不会调度其运行。
暂停状态:进程收到有关信号就会进入暂停状态,可用SIGCONT进行唤醒
僵死状态:进程停止运行
不可中断睡眠状态:使用wake_up()函数明确唤醒时才能转换到可运行的就绪状态。
图1.进程的5种运行状态
图2.进程之间的转换
有了这些基础知识,然后我们就可以开始进行创建新进程的分析了。首先,我们接触到一个系统调用函数fork,调用这个函数的时候,系统将会从用户态到内核态的切换。那么在程序中调用系统调用fork需要经过哪些步骤呢?目前主要有这么几个步骤:
1. 调用fork这个系统调用,首先是软中断,堆栈切换到内核堆栈,保存相应的寄存器,进行执行地址的跳转。
2. 查询相关的中断向量表,找到中断向量表中的函数名称,这里fork对应的函数名称是sys_clone函数。
3. 执行sys_clone,这个函数不是直接执行,而是都会执行一个do_fork函数,产生一个新的的进程。
下面是do_fork()的解释
do_fork()
{
copy_process(....){ //复制进程相关信息
dup_tusk_struct(current){ //复制PCB
alloc_task_struct_node() //相关PCB指针赋值
arch_dup_task_struct()
alloc_thread_info_node() //创建8KB大小的内存空间放置stack以及thread_info,返回stack起始地址ti
setup_thread_info() //初始化thread_info
}
retval=copy_thread(P) //设置新进程的起始地址,新进程的内核堆栈
}
}
下面是copy_thread()的代码解释
copy_thread()
p->thread.ip=ret_from_fork //入口地址
p->thread.sp=childregs //将新进程的内核堆栈指向
childregs=current_pt_regs //拷贝父进程内核堆栈数据
childregs->sp=sp; //将拷贝的sp设置为传入的sp
此时子进程的内核堆栈如下:
stack(ti),childregs : current_pt_regs 复制
ax=0; 修改ax
sp=sp; 修改sp
最后再进行一下跳转,并将所有子进程压入栈中的数据进行弹出,这样子进程的所有数据都有了,就可以自己运行了。
说了这么多理论,下面来开始我们的实验,进行一下验证。
图3.首先在test.c文件中写入Fork函数
图4.导入相应的执行命令
图5.编译运行
图6.编译运行完的结果,证明新进程已经建立
图7.调试模式启动
图8.在相应的执行行上打印上断点,主要针对fork功能打断点
图9.调试过程中
总结:fork创建子进程也是一种特殊的系统调用,在执行这个系统调用之前,要进行保护现场操作,也就是要对有关寄存器的值进行存储,当新进程创建完成以后可以回到此处继续执行。在保护好现场以后,就是查找sys_call_table里面的有关信息,这样就能根据调用号来找到相应的入口地址。再通过函数入口地址来进行处理,处理过程也是拷贝父进程的有关信息,然后加上自己独特的信息,这就构成新的进程,这也是fork的整个工作流程。