《Linux操作系统分析》之分析Linux内核创建一个新进程的过程

时间:2022-10-23 16:34:53

本篇文章通过fork函数的调用,来说明在Linux系统中创建一个新进程需要经历的过程。

相关知识:

首先关于这篇文章会介绍一些用到的知识。

一、进程、轻量级进程、线程

进程是程序执行的一个实例。进程的目的就是担当分配系统资源的实体。

两个轻量级进程基本可以共享一些资源,比如数据空间、打开的文件等。只要其中的一个修改共享资源,另一个就立马查看这种修改。

线程可以由内核独立的调度。线程之间可以通过简单地共享同一内存地址、同一打开文件集等来访问相同的应用程序数据结构集。

二、进程描述符

进程描述符都是task_struc类型结构。它包含了与进程相关的所有信息。

《Linux操作系统分析》之分析Linux内核创建一个新进程的过程

三、进程状态

状态以及之间的转换关系如图所示:

《Linux操作系统分析》之分析Linux内核创建一个新进程的过程

四、fork、vfork和clone

fork()函数复制时将父进程的所有资源都通过复制数据结构进行复制,然后传递给子进程,故fork()函数不带参数;clone()函数则是将部分父进程的资源的数据结构进行复制,复制哪些资源是可选择的,通过参数设定,故clone()函数带参数。fork()可以看出是完全版的clone(),而clone()克隆的只是fork()的一部分。为了提高系统的效率,后来的Linux设计者又增加了一个系统调用vfork()。vfork()所创建的不是进程而是线程,它所复制的是除了任务结构体和系统堆栈之外的所有资源的数据结构,而任务结构体和系统堆栈是与父进程共用的。

分析过程:

我们将Ubuntu中的menuOS进行更新

git clone https://github.com/mengning/menu.git
然后重新 make rootfs。结果如图:

《Linux操作系统分析》之分析Linux内核创建一个新进程的过程

在menuOS中添加了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");
}
}
然后在gdb中进行调试,过程与几篇博客相同,请参考博客: 《Linux操作系统分析》之跟踪分析Linux内核的启动过程 。设置的断点如图:

《Linux操作系统分析》之分析Linux内核创建一个新进程的过程

追踪调试结果如下:

《Linux操作系统分析》之分析Linux内核创建一个新进程的过程

通过上面断点的出现位置,我们知道一次进程的创建大致是这样的一个流程:

sys_clone—>do_fork—>copy_process—>dup_task_struct—>copy_thread—>ret_from_fork

我们大概看一下每个函数的执行:

先看fork函数:

#ifdef __ARCH_WANT_SYS_FORK
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
return do_fork(SIGCHLD, 0, 0, NULL, NULL);
#else
/* can not support in nommu mode */
return -EINVAL;
#endif
}
#endif

//vfork
#ifdef __ARCH_WANT_SYS_VFORK
SYSCALL_DEFINE0(vfork)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
0, NULL, NULL);
}
#endif

//clone
#ifdef __ARCH_WANT_SYS_CLONE
#ifdef CONFIG_CLONE_BACKWARDS
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int, tls_val,
int __user *, child_tidptr)
#elif defined(CONFIG_CLONE_BACKWARDS2)
SYSCALL_DEFINE5(clone, unsigned long, newsp, unsigned long, clone_flags,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
#elif defined(CONFIG_CLONE_BACKWARDS3)
SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp,
int, stack_size,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
#else
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
#endif
{
return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
}
#endif
我们看上面的代码知道三种方法创建的进程都是调用do_fork()实现的。

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;

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);//复制进程描述符,copy_process()的返回值是一个 task_struct 指针。

if (!IS_ERR(p)) {
struct completion vfork;
struct pid *pid;

trace_sched_process_fork(current, p);

pid = get_task_pid(p, PIDTYPE_PID);//得到新创建进程的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) {//如果是 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;
}
在上面我们看到首先调用copy_process 为子进程复制出一份进程信息,在调用 wake_up_new_task 将子进程加入调度器,为之分配 CPU。我们看一下copy_process的代码:

static struct task_struct *copy_process(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *child_tidptr,
struct pid *pid,
int trace)
{
int retval;
struct task_struct *p;

//省略

retval = security_task_create(clone_flags);//security_task_create 的作用是对 clone_flags 的设置的标志进 行安全检查,查看系统是否满足进程创建所需要的存储空间,用户配额限制。
if (retval)
goto fork_out;

retval = -ENOMEM;
p = dup_task_struct(current); //复制当前的 task_struct
if (!p)
goto fork_out;

//省略

spin_lock_init(&p->alloc_lock);//初始化一些资源
init_sigpending(&p->pending);

//省略

/* Perform scheduler related setup. Assign this task to a CPU. */
retval = sched_fork(clone_flags, p);//初始化进程数据结构,并把进程状态设置为 TASK_RUNNING
if (retval)
goto bad_fork_cleanup_policy;

retval = perf_event_init_task(p);
if (retval)
goto bad_fork_cleanup_policy;
retval = audit_alloc(p);
if (retval)
goto bad_fork_cleanup_perf;
/* copy all the process information */ //复制进程的所有信息
shm_init_task(p);
retval = copy_semundo(clone_flags, p);
if (retval)
goto bad_fork_cleanup_audit;
retval = copy_files(clone_flags, p);
if (retval)
goto bad_fork_cleanup_semundo;
retval = copy_fs(clone_flags, p);
if (retval)
goto bad_fork_cleanup_files;
retval = copy_sighand(clone_flags, p);
if (retval)
goto bad_fork_cleanup_fs;
retval = copy_signal(clone_flags, p);
if (retval)
goto bad_fork_cleanup_sighand;
retval = copy_mm(clone_flags, p);
if (retval)
goto bad_fork_cleanup_signal;
retval = copy_namespaces(clone_flags, p);
if (retval)
goto bad_fork_cleanup_mm;
retval = copy_io(clone_flags, p);
if (retval)
goto bad_fork_cleanup_namespaces;
retval = copy_thread(clone_flags, stack_start, stack_size, p);//初始化子进程内核栈
if (retval)
goto bad_fork_cleanup_io;

if (pid != &init_struct_pid) {
retval = -ENOMEM;
pid = alloc_pid(p->nsproxy->pid_ns_for_children);//为新进程分配新的 pid
if (!pid)
goto bad_fork_cleanup_io;
}

//省略

/* ok, now we should be set up.. */
p->pid = pid_nr(pid); //设置子进程 pid

//省略

return p;
//下面是各跳转函数的定义处,省略
}

在copy_process中会调用 dup_task_struct(current); 最终执行完dup_task_struct之后,子进程与父进程除了tsk->stack指针不同之外,其他全部都一样 ,然后 调用 sched_fork 初始化进程数据结构,并把进程状态设置为 TASK_RUNNING,再接着 调用 copy_thread (clone_flags, stack_start, stack_size, p);。下面看下copy_thread函数代码:

int copy_thread(unsigned long clone_flags, unsigned long sp,
unsigned long arg, struct task_struct *p)
{
struct pt_regs *childregs = task_pt_regs(p);
struct task_struct *tsk;
int err;

//省略

*childregs = *current_pt_regs(); //将当前寄存器信息复制给子进程
childregs->ax = 0;//子进程返回0
if (sp)
childregs->sp = sp;

p->thread.ip = (unsigned long) ret_from_fork;//将子进程的ip设置为 ret_from_fork,因此调度后子进程从ret_from_fork开始执行。

//省略

return err;
}
在上面我们知道子进程返回0的原因,以及将子进程ip 设置为ret_from_fork函数首地址,子进程将从ret_from_fork开始执行。
总结:

一、对于每个进程来说,Linux都把两个不同的数据结构紧凑地存放在一个单独为进程分配的存储区域内:一个是内核态的进程堆栈,另一个是紧挨着进程描述符的小数据节后thread_info,叫做线程描述符。(数据类型是union,通常是8192字节即两个页框)

union thread_union {
struct thread_info thread_info;//<span style="font-family: Arial;">thread_info是52字节长</span>
unsigned long stack[THREAD_SIZE/sizeof(long)];
};

线程描述符驻留于这个内存区的开始,而栈从末端(高地址)开始增长。因为thread_info是52字节长,所以内核栈能扩展到8140字节。

《Linux操作系统分析》之分析Linux内核创建一个新进程的过程

二、当一个进程创建时,它与父进程相同。它接受父进程地址空间的一个(逻辑)拷贝,并从进程创建系统调用的下一条指令开始执行与父进程相同的代码。尽管父子进程可以共享程序代码的页,但是它们各自有独立的数据拷贝(栈和堆),因此子进程对一个内存单元的修改对父进程是不可见的(反之亦然)。

三、进程的创建过程大概就是这样的:sys_clone—>do_fork—>copy_process—>dup_task_struct—>copy_thread—>ret_from_fork。

备注:

杨峻鹏 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000