#####################################
作者:张卓
原创作品转载请注明出处:《Linux操作系统分析》MOOC课程 http://www.xuetangx.com/courses/course-v1:ustcX+USTC001+_/about
#####################################
一 进程的描述
1. 进程控制块PCB——task_struct
为了管理进程,内核必须对每个进程进行清晰的描述,进程描述符提供了内核所需了解的进程信息。task_struct 数据结构很庞大, 比较重要的定义有:
● 进程状态
○ Linux进程的状态与操作系统原理中的描述的进程状态似乎有所不同,比如就绪状态和运行状态都是TASK_RUNNING。
● 进程的标示pid
● 所有进程链表struct list_head tasks;
○ 内核的双向循环链表的实现方法 - 一个更简略的双向循环链表
● 程序创建的进程具有父子关系,在编程时往往需要引用这样的父子关系。进程描述符中有几个域用来表示这样的关系
● Linux为每个进程分配一个8KB大小的内存区域,用于存放该进程两个不同的数据结构:Thread_info和进程的内核堆栈
○ 进程处于内核态时使用,不同于用户态堆栈,即PCB中指定了内核栈
○ 内核控制路径所用的堆栈很少,因此对栈和Thread_info来说,8KB足够了
● struct thread_struct thread; //CPU-specific state of this task
● 文件系统和文件描述符
● 内存管理——进程的地址空间
2.task_struct 结构体图示:
3. 进程状态
进程根据运行状态可分为: 就绪态、运行态、可中断睡眠态、不可中断睡眠态和僵死态。
每个状态都对应一个宏定义,定义的宏在:
include/linux/sched.h:
203 #define TASK_RUNNING 0
204 #define TASK_INTERRUPTIBLE 1
205 #define TASK_UNINTERRUPTIBLE 2
208 /* in tsk->exit_state */
209 #define EXIT_DEAD 16
210 #define EXIT_ZOMBIE 32
进程状态转化图如下:
2. 进程的创建
1. fork一个进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char * argv[])
{
int pid;
/* fork another process */
pid = fork();
if (pid < 0)
{
/* error occurred */
fprintf(stderr,"Fork Failed!");
exit(-1);
}
else if (pid == 0)
{
/* child process */
printf("This is Child Process!\n");
}
else
{
/* parent process */
printf("This is Parent Process!\n");
/* parent will wait for the child to complete*/
wait(NULL);
printf("Child Complete!\n");
}
}
上面一段小程序在用户态创建一个子进程。调用fork()函数之后,会有两次返回:一次在父进程中返回子进程的PID,一次在子进程中返回0。Linux通过复制父进程来创建一个新进程,我们可以简单的猜想一下,调用fork()之后,陷入内核主要做了什么?简单设想一下内核执行过程的框架:
○ 复制一个PCB——task_struct
○ 要给新进程分配一个新的内核堆栈
○ 要修改复制过来的进程数据,比如pid、进程链表等等
2. 创建一个新进程在内核中的执行过程
fork、vfork 和 clone三个系统调用都可以创建一个新进程,而且都是通过调用do_fork来实现进程的创建; 上面程序中调用fork(),调用过程实际是:
下面我们直接分析内核创建新进程的核心代码:
kernel/fork.c:
do_fork():
...
copy_process(): /* 在do_fork()调用copy_process()复制老进程的寄存器,所有适当的进程环境 */
dup_task_struct(): /* 在copy_process()中调用dup_task_struct(),复制task_struct */
alloc_thread_info_node()
arch_dup_task_struct()
setup_thread_stack()
copy_files()
copy_fs()
...
copy_thread()(arch/x86/kernel/process_32.c ):
*childregs = *current_pt_regs(); /*复制内核堆栈的一部分(struct pt_rergs) ,int指令和SAVE_ALL压到内核栈的内容*/
childregs->ax= 0; /* 子进程的返回值为什么是0,这就是答案*/
p->thread.ip = (unsigned) ret_from_fork;
/* 拷贝内核堆栈数据和指定新进程的第一条指令地址 */
/*对创建的进程进行一些错误检查, 检查标志位;最后唤醒新进程*/
if (!IS_ERR(p)) {
...
wake_up_new_task(p); /*首次唤醒创建的进程,同时完成一些初始化调度的所需的工作,并将进程放入运行队列中*/
...
} else {
nr = PTR_ERR(p);
}
return nr; /* 这里返回的就是子进程的PID */
在调用copy_thread()的时候,子进程thread.ip 被赋值为ret_from_fork的地址,所以子进程开始执行的第一条指令就是ret_from_fork.
arch/x86/kernel/entry_32.S:
ENTRY(ret_from_fork)
子进程执行完ret_form_fork之后,将又一次返回用户空间,且返回值为0。
下面附上do_fork()函数的整个代码:
linux-3.18.6/kernel/fork.c:
/*
* Ok, this is the main fork-routine.
*
* It copies the process, and if successful kick-starts
* it and waits for it to finish using the VM if required.
*/
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
{
struct task_struct *p;
int trace = 0;
long nr;
/*
* Determine whether and which event to report to ptracer. When
* called from kernel_thread or CLONE_UNTRACED is explicitly
* requested, no event is reported; otherwise, report if the event
* for the type of forking is enabled.
*/
if (!(clone_flags & CLONE_UNTRACED)) {
if (clone_flags & CLONE_VFORK)
trace = PTRACE_EVENT_VFORK;
else if ((clone_flags & CSIGNAL) != SIGCHLD)
trace = PTRACE_EVENT_CLONE;
else
trace = PTRACE_EVENT_FORK;
if (likely(!ptrace_event_enabled(current, trace)))
trace = 0;
}
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace);
/*
* Do this prior waking up the new thread - the thread pointer
* might get invalid after that point, if the thread exits quickly.
*/
if (!IS_ERR(p)) {
struct completion vfork;
struct pid *pid;
trace_sched_process_fork(current, p);
pid = get_task_pid(p, PIDTYPE_PID);
nr = pid_vnr(pid);
if (clone_flags & CLONE_PARENT_SETTID)
put_user(nr, parent_tidptr);
if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
get_task_struct(p);
}
wake_up_new_task(p);
/* forking complete and child started to run, tell ptracer */
if (unlikely(trace))
ptrace_event_pid(trace, pid);
if (clone_flags & CLONE_VFORK) {
if (!wait_for_vfork_done(p, &vfork))
ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
}
put_pid(pid);
} else {
nr = PTR_ERR(p);
}
return nr;
}
3. GDB 跟踪调试
对于上面的代码分析,我们可以用gdb设断点跟踪,加强我们对fork()创建新建的认识。