章强 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
进程描述
进程控制块PCB——task_struct,也叫进程描述符,为了管理进程,内核必须对每个进程进行清晰的描述,进程描述符提供了内核所需了解的进程信息。
struct task_struct数据结构很庞大:可以看到state进程状态,stack内核堆栈,flag进程标识符,CONFIG_SMP条件编译(多处理器用到)
进程的内存地址空间:
下图是抽象图:task进程链表管理,tty控制台,fs文件系统,files打开的文件描述符,mm内存管理描述,signal进程间通信信号
linux的进程状态与操作系统不同,就绪状态和运行状态都是TASK_RUNNING,fork创建好新进程变成就绪态,调度器选择了进程后变成运行态。
进程在TASK_RUNNING状态下表示其是可运行的,根据cpu是否实际执行该任务来划分是就绪态还是运行态。
正在运行进程调用do exit会变成task_zombie僵尸进程由系统处理掉
正在运行进程等待资源会进入阻塞态,可用时被唤醒进入就绪态。
进程pid
双向链表
程序创建的进程具有父子关系,在编程时往往需要引用这样的父子关系。进程描述符中有几个域用来表示这样的关系(都是通过双向链表来连接起来的)
内核处理过程
(1) do_fork
- 调用
copy_process
,将当前进程复制一份出来给子进程,并且为子进程设置相应地上下文信息。 - 调用
wake_up_new_task
,将子进程放入调度器的队列中,此时的子进程就可以被调度进程选中运行。
(2) copy_process
- 创建进程描述符以及子进程所需要的其他所有数据结构,为子进程准备运行环境
- 调用
dup_task_struct
复制一份task_struct
结构体,作为子进程的进程描述符。 - 复制所有的进程信息
- 调用
copy_thread
,设置子进程的堆栈信息,为子进程分配一个pid。
(3) dup_ task_ struct
- 先调用
alloc_task_struct_node
分配一个task_struct
结构体。 - 调用
alloc_thread_info_node
,分配了一个union。这里分配了一个thread_info
结构体,还分配了一个stack数组。返回值为ti,实际上就是栈底。 tsk->stack = ti
将栈底的地址赋给task的stack变量。- 最后为子进程分配了内核栈空间。
- 执行完
dup_task_struct
之后,子进程和父进程的task结构体,除了stack指针之外,完全相同。
(4) copy_thread
- 获取子进程寄存器信息的存放位置
- 对子进程的thread.sp赋值,将来子进程运行,这就是子进程的esp寄存器的值。
- 如果是创建内核线程,那么它的运行位置是
ret_from_kernel_thread
, - 将这段代码的地址赋给thread.ip,之后准备其他寄存器信息,退出 - 将父进程的寄存器信息复制给子进程。
- 将子进程的eax寄存器值设置为0,所以fork调用在子进程中的返回值为0.
- 子进程从
ret_from_fork
开始执行,所以它的地址赋给thread.ip,也就是将来的eip寄存器。
(5) 运行新进程:从ret_from_fork处开始执行
dup_task_struct
中为其分配了新的堆栈copy_process
中调用了sched_fork
,将其置为TASK_RUNNINGcopy_thread
中将父进程的寄存器上下文复制给子进程,这是非常关键的一步,这里保证了父子进程的堆栈信息是一致的。- 将
ret_from_fork
的地址设置为eip寄存器的值,这是子进程的第一条指令。
进程创建
创建新进程是通过复制当前进程实现的,fork、vfork和clone三个系统调用都可以创建一个新进程,而且都是通过调用do_fork来实现进程的创建。
do_fork主要是复制了父进程的task_struct,然后修改必要的信息,从而得到子进程的task_struct。
刚fork出来的子进程是从ret_from_fork开始执行的,然后跳转到syscall_exit,从系统调用中返回。
实验截图
cd LinuxKernel
rm menu -rf
cd ..
cp -rf Code/shiyanlou_cs195/menu LinuxKernel
cd LinuxKernel/menu
mv test_fork.c test.c
make rootfs
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S
在新窗口打开gdb
$ gdb
$ file linux-3.18.6/vmlinux
$ target remote:1234
设置断点
b sys_clone
b do_fork
b dup_task_struct
b copy_process
b copy_thread
b ret_from_fork
最后跳转到syscall_exit
总结
新进程的执行源于以下过程:
1. dup_task_struct中为其分配了新的堆栈。
2. 调用了sched_fork,将其置为TASK_RUNNING。
3. copy_thread中将父进程的寄存器上下文复制给子进程,保证了父子进程的堆栈信息是一致的。
4. 将ret_from_fork的地址设置为eip寄存器的值。
最终子进程从ret_from_fork开始执行。