Linux 进程管理之进程、线程

系统 Linux
Unix 中关于进程的创建分为2个步骤:fork() 和 exec() (这里 exec 指 exec一系列函数,因为内核实现了多个函数,比如 execv 等)。

进程创建

Unix 中关于进程的创建分为2个步骤:fork() 和 exec() (这里 exec 指 exec一系列函数,因为内核实现了多个函数,比如 execv 等)。

  • fork() 调用是通过拷贝当前进程来创建子进程。此时父子进程的区别在于pid(本进程号),ppid(父进程号)和一些资源和统计量。
  • exec() 函数用于加载可执行文件开始运行。

以上2个函数完成了进程的创建过程。

创建进程(线程)的方式有3种:fork()、vfork()、clone()。

fork 原理

有关 fork 的系统调用如下

#include <unistd.h>
pid_t fork(void);

fork() 系统调用返回信息,具体描述如下:

  • 返回值为-1时,表示创建失败。
  • 返回值为0时,返回到新创建的子进程。
  • 返回值大于0时,返回父进程或调用者。该值为新创建的子进程的进程ID。

当用户调用 fork()时,会进入系统调用 sys_fork()

int sys_fork(struct pt_regs *regs)
{
return do_fork(SIGCHLD, regs->sp, regs, 0, NULL, NULL);
}
do_fork 的第一个参数 clone_flags 可知,do_fork 只传递了一个当子进程去世时向父进程发送的信号 SIGCHLD。

参数 clone_flags 有2部分组成,最低字节为信号类型,用于表示子进程去世时向父进程发送的信号。高位表示的是资源和特性的标志位,比如:

#define CSIGNAL 0x000000ff
#define CLONE_VM 0x00000100
#define CLONE_FS 0x00000200
#define CLONE_FILES 0x00000400
#define CLONE_SIGHAND 0x00000800
#define CLONE_PTRACE 0x00002000
#define CLONE_VFORK 0x00004000
...

所以对于 fork,这一部分资源标志位全为0,表示对有关资源要进行复制而不是通过增加引用计数进行指针共享。

long do_fork(unsigned long clone_flags, ////资源标志
unsigned long stack_start, //子进程用户态堆栈地址
struct pt_regs *regs, //寄存器集合指针
unsigned long stack_size, //用户态下栈大小,该参数通常是不必要的,为0
int __user *parent_tidptr, //父进程在用户态下的pid的地址
int __user *child_tidptr) //子进程在用户态下pid的地址
{
struct task_struct *p;
int trace = 0;
long nr;
...
//复制子进程,为子进程复制出一份进程信息
p = copy_process(clone_flags, stack_start, regs, stack_size,
child_tidptr, NULL);
if (!IS_ERR(p)) {
struct completion vfork;
//fork系统调用要返回新进程的PID,如果设置了CLONE_NEWPID标志,fork操作可能创建了新的PID命名空间,此时要返回发出fork系统调用的进程所在命名空间的进程ID
nr = (clone_flags & CLONE_NEWPID) ?
task_pid_nr_ns(p, current->nsproxy->pid_ns) :
task_pid_vnr(p);
...
//将任务放入运行队列并将其唤醒
wake_up_new_task(p, clone_flags);
...
/* 如果是 vfork,将父进程加入至等待队列,等待子进程完成 */
if (clone_flags & CLONE_VFORK) {
freezer_do_not_count();
wait_for_completion(&vfork);
freezer_count();
if (unlikely (current->ptrace & PT_TRACE_VFORK_DONE)) {
current->ptrace_message = nr;
ptrace_notify ((PTRACE_EVENT_VFORK_DONE << 8) | SIGTRAP);
}
}
} else {
nr = PTR_ERR(p);
}
//返回子进程pid
return nr;
}

do_fork 的工作流程如下:

  • 调用 copy_process 为子进程复制一份描述符信息。
  • 将子进程加入运行队列并将其唤醒运行。
  • 若是调用 vfork() 则父进程等待子进程执行完成。

该函数的流程如下:

在 do_fork中,其核心处理程序为 copy_process,其实现如下

static struct task_struct *copy_process(unsigned long clone_flags,
unsigned long stack_start,
struct pt_regs *regs,
unsigned long stack_size,
int __user *child_tidptr,
struct pid *pid)
{
int retval;
struct task_struct *p;
int cgroup_callbacks_done = 0;

/* CLONE_FS 不能与 CLONE_NEWNS 同时设置 */
if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
return ERR_PTR(-EINVAL);


/* 创建线程时线程之间要共享信号处理函数 */
if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
return ERR_PTR(-EINVAL);


/*
* 父子进程共享信号处理函数时必须共享内存地址空间
* 这就是为什么fork出来的父子进程有其独立的信号处理函数,因为他们的内存地址空间不同
*/
if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
return ERR_PTR(-EINVAL);

...

retval = -ENOMEM;
//为新进程创建一个内核栈,此时父子进程的描述符完全相同
p = dup_task_struct(current);


rt_mutex_init_task(p);


retval = -EAGAIN;
/*
user指向user_struct。一个用户常常有多个进程,所以用户信息不属于某个专一的进程。属于同一个
用户的进程就可以通过指针user共享这些信息。每个用户只有一个user_struct结构。结构中的__count,
对属于该用户的进程数量计数。当然,内核线程并不属于某个用户,自然task_struct中的user指引为0.
校验该用户拥有的进程数有没有超过该用户所拥有的进程数量的限制。
*/
if (atomic_read(&p->user->processes) >=
p->signal->rlim[RLIMIT_NPROC].rlim_cur) {
if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) &&
p->user != current->nsproxy->user_ns->root_user)
goto bad_fork_free;
}
//对该user_struct结构的引用计数加1;对该用户所拥有的进程总数量加1
atomic_inc(&p->user->__count);
atomic_inc(&p->user->processes);
get_group_info(p->group_info);


//检测系统中进程的总数量(所有用户的进程数加系统的内核线程数)是否超过了max_threads所规定的进程最大数
if (nr_threads >= max_threads)
goto bad_fork_cleanup_count;

/* 如果实现新进程的执行域和可执行格式的内核函数都包含在内核模块中,则递增其使用计数 */
if (!try_module_get(task_thread_info(p)->exec_domain->module))
goto bad_fork_cleanup_count;

if (p->binfmt && !try_module_get(p->binfmt->module))
goto bad_fork_cleanup_put_domain;

p->did_exec = 0;
delayacct_tsk_init(p); /* Must remain after dup_task_struct() */
//将从do_fork()传递来的的clone_flags赋值给子进程描述符中的对应字段
copy_flags(clone_flags, p);
/* 初始化子进程的子进程链表和兄弟进程链表为空 */
INIT_LIST_HEAD(&p->children);
INIT_LIST_HEAD(&p->sibling);
p->vfork_done = NULL;
/* 初始化分配锁,此锁用于保护分配内存,文件,文件系统等操作 */
spin_lock_init(&p->alloc_lock);

clear_tsk_thread_flag(p, TIF_SIGPENDING);
/* 信号列表初始化,此列表保存被挂起的信号 */
init_sigpending(&p->pending);
//初始化其中的各个字段,使得子进程和父进程逐渐区别出来
p->utime = cputime_zero;
p->stime = cputime_zero;
...


//调用sched_fork函数执行调度器相关的设置,为这个新进程分配CPU,使得子进程的进程状态为TASK_RUNNING。并禁止内核抢占
sched_fork(p, clone_flags);
/*复制进程的资源信息比如打开的文件、文件系统信息,信号处理函数、进程地址空间、命令空间等.
至于这些信息时父子共享还是进行拷贝,根据clone_flags的具体取值来为子进程拷贝或共享父进程的某些数据结构
*/
security_task_alloc(p);
audit_alloc(p);
copy_semundo(clone_flags, p);
copy_files(clone_flags, p);
copy_fs(clone_flags, p);
/* 判断是否设置 CLONE_SIGHAND ,如果是(线程必须为是),增加父进行的sighand引用计数,如果否(创建的必定是子进程),将父线程的sighand_struct复制到子进程中 */
copy_sighand(clone_flags, p);
/* 如果创建的是线程,直接返回0,如果创建的是进程,则会将父进程的信号屏蔽和安排复制到子进程中 */
copy_signal(clone_flags, p);
/*
若clone_flags 不存在 CLONE_VM 时会对父进程 mm_struct 结构进行复制,对其的复制不仅局限于这个数据结构的复制,还包括了更深层次的数据结构的复制。
其中最重要的是vm_area_struct数据结构和页面映射表的复制,这都是由dup_mmap()复制的
*/
copy_mm(clone_flags, p);
copy_keys(clone_flags, p);
copy_namespaces(clone_flags, p); /*
复制线程。通过copy_threads()函数更新子进程的内核栈和寄存器中的值,在之前的dup_task_struct()中只是为子进程创建一个内核栈,
内核栈是空的,并没有实际意义。
当父进程发出clone系统调用时,内核会将那个时候CPU中寄存器的值保存在父进程的内核栈中。
这里就是使用父进程内核栈中的值来更新子进程寄存器中的值。特别的,内核将子进程eax寄存器中的值强制赋值为0,
这也就是为什么使用fork()时子进程返回值是0。另外,子进程的对应的thread_info结构中的esp字段会被初始化为子进程内核栈的基址

copy_thread函数会将父进程的thread_struct和内核栈数据复制到子进程中,并将子进程的返回值置为0(x86返回值保存在eax中,arm保存在r0中,即把eax或者r0所在的内核栈数据置为0)
copy_thread函数还会将子进程的eip寄存器值设置为ret_from_fork()的地址,即当子进程首次被调用就立即执行系统调用clone返回。
所以应用层调用fork()函数后,子进程返回0,父进程返回子进程ID(返回子进程ID在之后代码中会实现)

*/
retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);
if (retval)
goto bad_fork_cleanup_namespaces;
/* 判断是不是init进程 */
if (pid != &init_struct_pid) {
retval = -ENOMEM;
/*分配pid
用alloc_pid函数为这个新进程分配一个pid,Linux系统内的pid是循环使用的,采用位图方式来管理。
简单的说,就是用每一位(bit)来标示该位所对应的pid是否被使用。
分配完毕后,判断pid是否分配成功。成功则赋给p->pid
*/
pid = alloc_pid(task_active_pid_ns(p));
if (!pid)
goto bad_fork_cleanup_namespaces;

if (clone_flags & CLONE_NEWPID) {
retval = pid_ns_prepare_proc(task_active_pid_ns(p));
if (retval < 0)
goto bad_fork_free_pid;
}
}
/* 将子进程的PID设置为分配的PID在全局namespace中分配的值,在不同namespace中进程的PID不同,而p->pid保存的是全局的namespace中所分配的PID */
p->pid = pid_nr(pid);
p->tgid = p->pid;
if (clone_flags & CLONE_THREAD)
p->tgid = current->tgid; /* 线程组的所有线程的tgid都一致,使用getpid返回的就是tgid */

/* 如果设置了CLONE_CHILD_SETTID则将task_struct中的set_child_tid指向用户空间的child_tidptr,否则置空 */
p->set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? child_tidptr : NULL;

/* 如果设置了CLONE_CHILD_CLEARTID则将task_struct中的clear_child_tid指向用户空间的child_tidptr,否则置空 */
p->clear_child_tid = (clone_flags & CLONE_CHILD_CLEARTID) ? child_tidptr: NULL;

...

/*
* 如果共享VM或者vfork创建,信号栈清空
*/
if ((clone_flags & (CLONE_VM|CLONE_VFORK)) == CLONE_VM)
p->sas_ss_sp = p->sas_ss_size = 0;


clear_tsk_thread_flag(p, TIF_SYSCALL_TRACE);

/*parent_exec_id 表示父进程的执行域,p->self_exec_id 是本进程(子进程)的执行域*/
p->parent_exec_id = p->self_exec_id;


//exit_signal 为本进程执行exit()系统调用时向父进程发出的信号,
p->exit_signal = (clone_flags & CLONE_THREAD) ? -1 : (clone_flags & CSIGNAL);
//pdeath_signal 为要求父进程在执行exit()时向本进程发出的信号
p->pdeath_signal = 0;
p->exit_state = 0;

p->group_leader = p;
/*进程创建后必须处于某一组中,这是通过task_struct 结构中的队列头thread_group
与父进程链接起来,形成一个进程组(注意,thread 并不单指线程,内核代码中经常用thread
通指所有的进程)*/
INIT_LIST_HEAD(&p->thread_group);
INIT_LIST_HEAD(&p->ptrace_children);
INIT_LIST_HEAD(&p->ptrace_list);

...

/* 将调用fork的进程为其父进程 */
if (clone_flags & (CLONE_PARENT|CLONE_THREAD))
/* 创建的是兄弟进程或者相同线程组线程,其父进程为父进程的父进程 */
p->real_parent = current->real_parent;
else
/* 创建的是子进程,父进程为父进程*/
p->real_parent = current;
p->parent = p->real_parent;

spin_lock(¤t->sighand->siglock);


/*
* 在fork之前,进程组和会话信号都需要送到父亲结点,而在fork之后,这些信号需要送到父亲和孩子结点。
* 如果我们在将新进程添加到进程组的过程中出现一个信号,而这个挂起信号会导致当前进程退出(current),我们的子进程就不能够被kill或者退出了
* 所以这里要检测父进程有没有信号被挂起。
*/
recalc_sigpending();
if (signal_pending(current)) {
spin_unlock(¤t->sighand->siglock);
write_unlock_irq(&tasklist_lock);
retval = -ERESTARTNOINTR;
goto bad_fork_free_pid;
}

if (clone_flags & CLONE_THREAD) {
...
}

if (likely(p->pid)) {
add_parent(p);
if (unlikely(p->ptrace & PT_PTRACED))
__ptrace_link(p, current->parent);

if (thread_group_leader(p)) {
if (clone_flags & CLONE_NEWPID)
p->nsproxy->pid_ns->child_reaper = p;

p->signal->tty = current->signal->tty;
set_task_pgrp(p, task_pgrp_nr(current));
set_task_session(p, task_session_nr(current));
attach_pid(p, PIDTYPE_PGID, task_pgrp(current));
attach_pid(p, PIDTYPE_SID, task_session(current));
/* 将此进程task_struct加入到task链表中 */
list_add_tail_rcu(&p->tasks, &init_task.tasks);
__get_cpu_var(process_counts)++;
}
attach_pid(p, PIDTYPE_PID, pid);
/* 当前系统进程数加1 */
nr_threads++;
}
/* 已创建的进程数量加1 */
total_forks++;
spin_unlock(¤t->sighand->siglock);
write_unlock_irq(&tasklist_lock);
proc_fork_connector(p);
cgroup_post_fork(p);
/*返回子进程的task_struct,通过sched_fork调用把子进程状态设置为可运行状态,但是子进程还没有运行,
后续子进程的调度运行取决于schedule()调度程序
*/
return p;

//错误处理
...
}

copy_process 处理流程如下:

  • 调用dup_task_struct为新进程创建一个内核栈、thread_info 和 task_struct 结构,这些信息和父进程内容相同。此时父子进程的描述符时完全相同的。
  • 对资源限制进行检查,保证新创建子进程后,当前用户所拥有的的进程数没有超过给它分配的资源的限制。
  • 对子进程的一些信息开始设置初始值,主要是一些统计信息等。
  • 调用 sched_fork 函数执行调度器相关的设置,为这个新进程分配 CPU,把子进程的进程状态为 TASK_RUNNING。
  • 复制进程的资源信息比如打开的文件、文件系统信息,信号处理函数、进程地址空间、命令空间等。
  • 调用 copy_thread 初始化子进程内核栈。
  • 为新进程分配并设置新的 pid。
  • 返回 task_struct 进程描述符。

该函数的流程如下:

通过上述 copy_process 可知,子进程完全复制复制了父进程的一些资源信息,如下图

dup_task_struct 完成了子进程内核栈的创建
static struct task_struct *dup_task_struct(struct task_struct *orig)
{
struct task_struct *tsk;
struct thread_info *ti;
int err;

prepare_to_copy(orig);

//分配一块task_struct
tsk = alloc_task_struct();

//分配一个 thread_info 节点,包含进程的内核栈,ti 为栈底
ti = alloc_thread_info(tsk);

//把父进程task_struct内容复制给子进程
*tsk = *orig;
//子进程的task_struct指向栈中的thread_info
tsk->stack = ti;

...
//把父进程thread_info复制给子进程的thread_info,然后子进程的thread_info指向子进程的task_struct
setup_thread_stack(tsk, orig);
...

return tsk;
}

dup_task_struct 为新进程创建一个内核栈、thread_info 和 task_struct 结构,这些结构中的信息完全复制了父进程信息,同时完成了 thread_info 和 task_struct 之间的关系,如下图

在 copy_process 中通过 dup_task_struct 为子进程分配了描述结构并初始化,完成内核栈的低端数据的初始化,而用作内核堆栈的高端复制初始化由 copy_thread 来完成。

int copy_thread(int nr, unsigned long clone_flags, unsigned long esp,
unsigned long unused,
struct task_struct * p, struct pt_regs * regs)
{
struct pt_regs * childregs;
struct task_struct *tsk;
int err;
//参数regs是保存这个cpu进入内核前夕各个寄存器中的内容而形成的一个pt_regs结构
childregs = task_pt_regs(p); //指向内核栈的最高地址
*childregs = *regs; //把寄存器中的值存放到内核栈的最高地址
//对子进程的内核栈寄存器中的值进行调整
childregs->eax = 0; //将eax设置0,子进程被调度运行从系统调用返回时,就返回该值,这也就为什么fork时子进程返回的是0
/*
将esp设置成参数的esp,因为它决定了进程在用户空间的堆栈位置.
对于clone()调用,参数esp是由调用者给定的。
对于fork() 和 vfork(), 参数esp来自do_fork()前夕的regs.esp, 所以实际上并未改变,还是执行父进程原来的用户空间的堆栈
*/
childregs->esp = esp;
/*
task_struct 中的thread,它本身是一个thread_struct结构,里面记录的是进程在切换时的(系统空间)堆栈指针,
取指令地址(也即是返回地址)等关键信息。在复制时原封不动的复制了,但是子进程也有自己的内核空间堆栈,因此
也需要进行调整。
*/
//将thread.esp设置成子进程系统空间栈中pt_regs结构的其实地址,就好像该子进程以前曾运行过,而在进入内核以后正要返回用户空间时被切换了一样
p->thread.esp = (unsigned long) childregs;
//esp0 指向子进程的系统空间堆栈的顶端。当一个进程被调度运行时,内核会将这个变量写入TSS 的 esp0 字段,表示这个进程进入0级运行时其堆栈的位置
p->thread.esp0 = (unsigned long) (childregs+1);
//eip表示当进程下一次被切换进行运行时的切入点,类似于函数调用或中断的返回地址。将此地址设置为ret_from_fork,使得子进程在首次调度运行时从这开始
p->thread.eip = (unsigned long) ret_from_fork;
//把段寄存器gs的值保存到p->thread.gs中
savesegment(gs,p->thread.gs);

//在父进程包含I/O访问许可权限位图的情况下,使新创建进程继承父进程的I/O访问许可权限位图
...

/*
* Set a new TLS for the child thread?
*/
//在参数clone_flags包含CLONE_SETTLS标记的情况下,设置进程的TLS
...
return err;
}

Linux 进程管理之进程、线程

进程创建


Unix 中关于进程的创建分为2个步骤:fork() 和 exec() (这里 exec 指 exec一系列函数,因为内核实现了多个函数,比如 execv 等)。


  • fork() 调用是通过拷贝当前进程来创建子进程。此时父子进程的区别在于pid(本进程号),ppid(父进程号)和一些资源和统计量。

  • exec() 函数用于加载可执行文件开始运行。


以上2个函数完成了进程的创建过程。


创建进程(线程)的方式有3种:fork()、vfork()、clone()。


fork 原理


有关 fork 的系统调用如下


#include <unistd.h>
pid_t fork(void);


fork() 系统调用返回信息,具体描述如下:

  • 返回值为-1时,表示创建失败。
  • 返回值为0时,返回到新创建的子进程。
  • 返回值大于0时,返回父进程或调用者。该值为新创建的子进程的进程ID。


当用户调用 fork()时,会进入系统调用 sys_fork()


int sys_fork(struct pt_regs *regs)
{
return do_fork(SIGCHLD, regs->sp, regs, 0, NULL, NULL);
}

从 do_fork 的第一个参数 clone_flags 可知,do_fork 只传递了一个当子进程去世时向父进程发送的信号 SIGCHLD。


参数 clone_flags 有2部分组成,最低字节为信号类型,用于表示子进程去世时向父进程发送的信号。高位表示的是资源和特性的标志位,比如:


#define CSIGNAL 0x000000ff 
#define CLONE_VM 0x00000100
#define CLONE_FS 0x00000200
#define CLONE_FILES 0x00000400
#define CLONE_SIGHAND 0x00000800
#define CLONE_PTRACE 0x00002000
#define CLONE_VFORK 0x00004000
...


所以对于 fork,这一部分资源标志位全为0,表示对有关资源要进行复制而不是通过增加引用计数进行指针共享。

long do_fork(unsigned long clone_flags, ////资源标志
unsigned long stack_start, //子进程用户态堆栈地址
struct pt_regs *regs, //寄存器集合指针
unsigned long stack_size, //用户态下栈大小,该参数通常是不必要的,为0
int __user *parent_tidptr, //父进程在用户态下的pid的地址
int __user *child_tidptr) //子进程在用户态下pid的地址
{
struct task_struct *p;
int trace = 0;
long nr;

...

//复制子进程,为子进程复制出一份进程信息
p = copy_process(clone_flags, stack_start, regs, stack_size,
child_tidptr, NULL);

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


//fork系统调用要返回新进程的PID,如果设置了CLONE_NEWPID标志,fork操作可能创建了新的PID命名空间,此时要返回发出fork系统调用的进程所在命名空间的进程ID
nr = (clone_flags & CLONE_NEWPID) ?
task_pid_nr_ns(p, current->nsproxy->pid_ns) :
task_pid_vnr(p);

...


//将任务放入运行队列并将其唤醒
wake_up_new_task(p, clone_flags);

...

/* 如果是 vfork,将父进程加入至等待队列,等待子进程完成 */
if (clone_flags & CLONE_VFORK) {
freezer_do_not_count();
wait_for_completion(&vfork);
freezer_count();
if (unlikely (current->ptrace & PT_TRACE_VFORK_DONE)) {
current->ptrace_message = nr;
ptrace_notify ((PTRACE_EVENT_VFORK_DONE << 8) | SIGTRAP);
}
}
} else {
nr = PTR_ERR(p);
}
//返回子进程pid
return nr;
}


do_fork 的工作流程如下:

  • 调用 copy_process 为子进程复制一份描述符信息。
  • 将子进程加入运行队列并将其唤醒运行
  • 若是调用 vfork() 则父进程等待子进程执行完成。


该函数的流程如下:

在 do_fork中,其核心处理程序为 copy_process,其实现如下

static struct task_struct *copy_process(unsigned long clone_flags,
unsigned long stack_start,
struct pt_regs *regs,
unsigned long stack_size,
int __user *child_tidptr,
struct pid *pid)
{
int retval;
struct task_struct *p;
int cgroup_callbacks_done = 0;

/* CLONE_FS 不能与 CLONE_NEWNS 同时设置 */
if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
return ERR_PTR(-EINVAL);


/* 创建线程时线程之间要共享信号处理函数 */
if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
return ERR_PTR(-EINVAL);


/*
* 父子进程共享信号处理函数时必须共享内存地址空间
* 这就是为什么fork出来的父子进程有其独立的信号处理函数,因为他们的内存地址空间不同
*/
if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
return ERR_PTR(-EINVAL);

...

retval = -ENOMEM;
//为新进程创建一个内核栈,此时父子进程的描述符完全相同
p = dup_task_struct(current);


rt_mutex_init_task(p);


retval = -EAGAIN;
/*
user指向user_struct。一个用户常常有多个进程,所以用户信息不属于某个专一的进程。属于同一个
用户的进程就可以通过指针user共享这些信息。每个用户只有一个user_struct结构。结构中的__count,
对属于该用户的进程数量计数。当然,内核线程并不属于某个用户,自然task_struct中的user指引为0.
校验该用户拥有的进程数有没有超过该用户所拥有的进程数量的限制。
*/
if (atomic_read(&p->user->processes) >=
p->signal->rlim[RLIMIT_NPROC].rlim_cur) {
if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) &&
p->user != current->nsproxy->user_ns->root_user)
goto bad_fork_free;
}
//对该user_struct结构的引用计数加1;对该用户所拥有的进程总数量加1
atomic_inc(&p->user->__count);
atomic_inc(&p->user->processes);
get_group_info(p->group_info);


//检测系统中进程的总数量(所有用户的进程数加系统的内核线程数)是否超过了max_threads所规定的进程最大数
if (nr_threads >= max_threads)
goto bad_fork_cleanup_count;

/* 如果实现新进程的执行域和可执行格式的内核函数都包含在内核模块中,则递增其使用计数 */
if (!try_module_get(task_thread_info(p)->exec_domain->module))
goto bad_fork_cleanup_count;

if (p->binfmt && !try_module_get(p->binfmt->module))
goto bad_fork_cleanup_put_domain;

p->did_exec = 0;
delayacct_tsk_init(p); /* Must remain after dup_task_struct() */
//将从do_fork()传递来的的clone_flags赋值给子进程描述符中的对应字段
copy_flags(clone_flags, p);
/* 初始化子进程的子进程链表和兄弟进程链表为空 */
INIT_LIST_HEAD(&p->children);
INIT_LIST_HEAD(&p->sibling);
p->vfork_done = NULL;
/* 初始化分配锁,此锁用于保护分配内存,文件,文件系统等操作 */
spin_lock_init(&p->alloc_lock);

clear_tsk_thread_flag(p, TIF_SIGPENDING);
/* 信号列表初始化,此列表保存被挂起的信号 */
init_sigpending(&p->pending);
//初始化其中的各个字段,使得子进程和父进程逐渐区别出来
p->utime = cputime_zero;
p->stime = cputime_zero;
...


//调用sched_fork函数执行调度器相关的设置,为这个新进程分配CPU,使得子进程的进程状态为TASK_RUNNING。并禁止内核抢占
sched_fork(p, clone_flags);
/*复制进程的资源信息比如打开的文件、文件系统信息,信号处理函数、进程地址空间、命令空间等.
至于这些信息时父子共享还是进行拷贝,根据clone_flags的具体取值来为子进程拷贝或共享父进程的某些数据结构
*/
security_task_alloc(p);
audit_alloc(p);
copy_semundo(clone_flags, p);
copy_files(clone_flags, p);
copy_fs(clone_flags, p);
/* 判断是否设置 CLONE_SIGHAND ,如果是(线程必须为是),增加父进行的sighand引用计数,如果否(创建的必定是子进程),将父线程的sighand_struct复制到子进程中 */
copy_sighand(clone_flags, p);
/* 如果创建的是线程,直接返回0,如果创建的是进程,则会将父进程的信号屏蔽和安排复制到子进程中 */
copy_signal(clone_flags, p);
/*
若clone_flags 不存在 CLONE_VM 时会对父进程 mm_struct 结构进行复制,对其的复制不仅局限于这个数据结构的复制,还包括了更深层次的数据结构的复制。
其中最重要的是vm_area_struct数据结构和页面映射表的复制,这都是由dup_mmap()复制的
*/
copy_mm(clone_flags, p);
copy_keys(clone_flags, p);
copy_namespaces(clone_flags, p); /*
复制线程。通过copy_threads()函数更新子进程的内核栈和寄存器中的值,在之前的dup_task_struct()中只是为子进程创建一个内核栈,
内核栈是空的,并没有实际意义。
当父进程发出clone系统调用时,内核会将那个时候CPU中寄存器的值保存在父进程的内核栈中。
这里就是使用父进程内核栈中的值来更新子进程寄存器中的值。特别的,内核将子进程eax寄存器中的值强制赋值为0,
这也就是为什么使用fork()时子进程返回值是0。另外,子进程的对应的thread_info结构中的esp字段会被初始化为子进程内核栈的基址

copy_thread函数会将父进程的thread_struct和内核栈数据复制到子进程中,并将子进程的返回值置为0(x86返回值保存在eax中,arm保存在r0中,即把eax或者r0所在的内核栈数据置为0)
copy_thread函数还会将子进程的eip寄存器值设置为ret_from_fork()的地址,即当子进程首次被调用就立即执行系统调用clone返回。
所以应用层调用fork()函数后,子进程返回0,父进程返回子进程ID(返回子进程ID在之后代码中会实现)

*/
retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);
if (retval)
goto bad_fork_cleanup_namespaces;
/* 判断是不是init进程 */
if (pid != &init_struct_pid) {
retval = -ENOMEM;
/*分配pid
用alloc_pid函数为这个新进程分配一个pid,Linux系统内的pid是循环使用的,采用位图方式来管理。
简单的说,就是用每一位(bit)来标示该位所对应的pid是否被使用。
分配完毕后,判断pid是否分配成功。成功则赋给p->pid
*/
pid = alloc_pid(task_active_pid_ns(p));
if (!pid)
goto bad_fork_cleanup_namespaces;

if (clone_flags & CLONE_NEWPID) {
retval = pid_ns_prepare_proc(task_active_pid_ns(p));
if (retval < 0)
goto bad_fork_free_pid;
}
}
/* 将子进程的PID设置为分配的PID在全局namespace中分配的值,在不同namespace中进程的PID不同,而p->pid保存的是全局的namespace中所分配的PID */
p->pid = pid_nr(pid);
p->tgid = p->pid;
if (clone_flags & CLONE_THREAD)
p->tgid = current->tgid; /* 线程组的所有线程的tgid都一致,使用getpid返回的就是tgid */

/* 如果设置了CLONE_CHILD_SETTID则将task_struct中的set_child_tid指向用户空间的child_tidptr,否则置空 */
p->set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? child_tidptr : NULL;

/* 如果设置了CLONE_CHILD_CLEARTID则将task_struct中的clear_child_tid指向用户空间的child_tidptr,否则置空 */
p->clear_child_tid = (clone_flags & CLONE_CHILD_CLEARTID) ? child_tidptr: NULL;

...

/*
* 如果共享VM或者vfork创建,信号栈清空
*/
if ((clone_flags & (CLONE_VM|CLONE_VFORK)) == CLONE_VM)
p->sas_ss_sp = p->sas_ss_size = 0;


clear_tsk_thread_flag(p, TIF_SYSCALL_TRACE);

/*parent_exec_id 表示父进程的执行域,p->self_exec_id 是本进程(子进程)的执行域*/
p->parent_exec_id = p->self_exec_id;


//exit_signal 为本进程执行exit()系统调用时向父进程发出的信号,
p->exit_signal = (clone_flags & CLONE_THREAD) ? -1 : (clone_flags & CSIGNAL);
//pdeath_signal 为要求父进程在执行exit()时向本进程发出的信号
p->pdeath_signal = 0;
p->exit_state = 0;

p->group_leader = p;
/*进程创建后必须处于某一组中,这是通过task_struct 结构中的队列头thread_group
与父进程链接起来,形成一个进程组(注意,thread 并不单指线程,内核代码中经常用thread
通指所有的进程)*/
INIT_LIST_HEAD(&p->thread_group);
INIT_LIST_HEAD(&p->ptrace_children);
INIT_LIST_HEAD(&p->ptrace_list);

...

/* 将调用fork的进程为其父进程 */
if (clone_flags & (CLONE_PARENT|CLONE_THREAD))
/* 创建的是兄弟进程或者相同线程组线程,其父进程为父进程的父进程 */
p->real_parent = current->real_parent;
else
/* 创建的是子进程,父进程为父进程*/
p->real_parent = current;
p->parent = p->real_parent;

spin_lock(¤t->sighand->siglock);


/*
* 在fork之前,进程组和会话信号都需要送到父亲结点,而在fork之后,这些信号需要送到父亲和孩子结点。
* 如果我们在将新进程添加到进程组的过程中出现一个信号,而这个挂起信号会导致当前进程退出(current),我们的子进程就不能够被kill或者退出了
* 所以这里要检测父进程有没有信号被挂起。
*/
recalc_sigpending();
if (signal_pending(current)) {
spin_unlock(¤t->sighand->siglock);
write_unlock_irq(&tasklist_lock);
retval = -ERESTARTNOINTR;
goto bad_fork_free_pid;
}

if (clone_flags & CLONE_THREAD) {
...
}

if (likely(p->pid)) {
add_parent(p);
if (unlikely(p->ptrace & PT_PTRACED))
__ptrace_link(p, current->parent);

if (thread_group_leader(p)) {
if (clone_flags & CLONE_NEWPID)
p->nsproxy->pid_ns->child_reaper = p;

p->signal->tty = current->signal->tty;
set_task_pgrp(p, task_pgrp_nr(current));
set_task_session(p, task_session_nr(current));
attach_pid(p, PIDTYPE_PGID, task_pgrp(current));
attach_pid(p, PIDTYPE_SID, task_session(current));
/* 将此进程task_struct加入到task链表中 */
list_add_tail_rcu(&p->tasks, &init_task.tasks);
__get_cpu_var(process_counts)++;
}
attach_pid(p, PIDTYPE_PID, pid);
/* 当前系统进程数加1 */
nr_threads++;
}
/* 已创建的进程数量加1 */
total_forks++;
spin_unlock(¤t->sighand->siglock);
write_unlock_irq(&tasklist_lock);
proc_fork_connector(p);
cgroup_post_fork(p);
/*返回子进程的task_struct,通过sched_fork调用把子进程状态设置为可运行状态,但是子进程还没有运行,
后续子进程的调度运行取决于schedule()调度程序
*/
return p;

//错误处理
...
}

copy_process 处理流程如下:

  • 调用dup_task_struct为新进程创建一个内核栈、thread_info 和 task_struct 结构,这些信息和父进程内容相同。此时父子进程的描述符时完全相同的。
  • 对资源限制进行检查,保证新创建子进程后,当前用户所拥有的的进程数没有超过给它分配的资源的限制。
  • 对子进程的一些信息开始设置初始值,主要是一些统计信息等。
  • 调用 sched_fork 函数执行调度器相关的设置,为这个新进程分配 CPU,把子进程的进程状态为 TASK_RUNNING。
  • 复制进程的资源信息比如打开的文件、文件系统信息,信号处理函数、进程地址空间、命令空间等。
  • 调用 copy_thread 初始化子进程内核栈。
  • 为新进程分配并设置新的 pid。
  • 返回 task_struct 进程描述符。


该函数的流程如下:

通过上述 copy_process 可知,子进程完全复制复制了父进程的一些资源信息,如下图

dup_task_struct 完成了子进程内核栈的创建
static struct task_struct *dup_task_struct(struct task_struct *orig)
{
struct task_struct *tsk;
struct thread_info *ti;
int err;

prepare_to_copy(orig);

//分配一块task_struct
tsk = alloc_task_struct();

//分配一个 thread_info 节点,包含进程的内核栈,ti 为栈底
ti = alloc_thread_info(tsk);

//把父进程task_struct内容复制给子进程
*tsk = *orig;
//子进程的task_struct指向栈中的thread_info
tsk->stack = ti;

...
//把父进程thread_info复制给子进程的thread_info,然后子进程的thread_info指向子进程的task_struct
setup_thread_stack(tsk, orig);
...

return tsk;
}

dup_task_struct 为新进程创建一个内核栈、thread_info 和 task_struct 结构,这些结构中的信息完全复制了父进程信息,同时完成了 thread_info 和 task_struct 之间的关系,如下图

在 copy_process 中通过 dup_task_struct 为子进程分配了描述结构并初始化,完成内核栈的低端数据的初始化,而用作内核堆栈的高端复制初始化由 copy_thread 来完成。


int copy_thread(int nr, unsigned long clone_flags, unsigned long esp,
unsigned long unused,
struct task_struct * p, struct pt_regs * regs)
{
struct pt_regs * childregs;
struct task_struct *tsk;
int err;
//参数regs是保存这个cpu进入内核前夕各个寄存器中的内容而形成的一个pt_regs结构
childregs = task_pt_regs(p); //指向内核栈的最高地址
*childregs = *regs; //把寄存器中的值存放到内核栈的最高地址
//对子进程的内核栈寄存器中的值进行调整
childregs->eax = 0; //将eax设置0,子进程被调度运行从系统调用返回时,就返回该值,这也就为什么fork时子进程返回的是0
/*
将esp设置成参数的esp,因为它决定了进程在用户空间的堆栈位置.
对于clone()调用,参数esp是由调用者给定的。
对于fork() 和 vfork(), 参数esp来自do_fork()前夕的regs.esp, 所以实际上并未改变,还是执行父进程原来的用户空间的堆栈
*/
childregs->esp = esp;
/*
task_struct 中的thread,它本身是一个thread_struct结构,里面记录的是进程在切换时的(系统空间)堆栈指针,
取指令地址(也即是返回地址)等关键信息。在复制时原封不动的复制了,但是子进程也有自己的内核空间堆栈,因此
也需要进行调整。
*/
//将thread.esp设置成子进程系统空间栈中pt_regs结构的其实地址,就好像该子进程以前曾运行过,而在进入内核以后正要返回用户空间时被切换了一样
p->thread.esp = (unsigned long) childregs;
//esp0 指向子进程的系统空间堆栈的顶端。当一个进程被调度运行时,内核会将这个变量写入TSS 的 esp0 字段,表示这个进程进入0级运行时其堆栈的位置
p->thread.esp0 = (unsigned long) (childregs+1);
//eip表示当进程下一次被切换进行运行时的切入点,类似于函数调用或中断的返回地址。将此地址设置为ret_from_fork,使得子进程在首次调度运行时从这开始
p->thread.eip = (unsigned long) ret_from_fork;
//把段寄存器gs的值保存到p->thread.gs中
savesegment(gs,p->thread.gs);

//在父进程包含I/O访问许可权限位图的情况下,使新创建进程继承父进程的I/O访问许可权限位图
...

/*
* Set a new TLS for the child thread?
*/
//在参数clone_flags包含CLONE_SETTLS标记的情况下,设置进程的TLS
...
return err;
}

通过 copy_thread 初始化子进程内核栈的高端地址,修改其中的寄存器,保证了子进程被调度运行返回时能够和父进程进行了区分。

我们知道应用程调用 fork() 会返回2次,父进程返回的是子进程的 id, 子进程返回0,那子进程是怎么返回的呢?

在 copy_thread 函数将子进程的 eip 寄存器值设置为 ret_from_fork 的地址,同时将 eax 寄存器中的值赋值为0(eax 记录的就是函数返回时的值)。

ENTRY(ret_from_fork)
CFI_STARTPROC
pushl %eax
CFI_ADJUST_CFA_OFFSET 4
call schedule_tail
GET_THREAD_INFO(%ebp)
popl %eax
CFI_ADJUST_CFA_OFFSET -4
pushl $0x0202 # Reset kernel eflags
CFI_ADJUST_CFA_OFFSET 4
popfl
CFI_ADJUST_CFA_OFFSET -4
jmp syscall_exit
CFI_ENDPROC
END(ret_from_fork)

当子进程被调度运行时,子进程进入 ret_from_fork,在调用完 schedule_tail 后调到 syscall_exit 结束系统调用返回到用户空间,用户空间从 eax 寄存器中获取返回值0,也即是调用 fork 的返回值。

写时拷贝

在 fork() 创建进程的过程中, Linux 采用了写时拷贝(copy-on-write)页的技术,该技术就是一种可以延迟拷贝或免除拷贝的技术。

其原理就是先通过复制页表项暂时共享这个物理内存页。当从父进程复制页表项时会把父进程的页表项改成写保护,然后把改成写保护的页表项设置到子进程的页表中。这样2个进程的页面都变成“只读”的了。当不管父进程还是子进程企图写入该页面时,都会引起一次页面异常,而页面异常处理程序会对此的反应是为其分配一个物理页,并把内容真正的复制到新的物理页面中。此时父子进程各自拥有自己的物理页面,然后将这2个页面表中相应的表项改成可写。

写时拷贝技术避免了在创建进程过程中进行大量根本就不会使用的数据进行拷贝而带来的开销。

fork() 的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符的过程。

有关写时拷贝技术原理如下

在 fork() 后子进程完全复制了父进程的页表,但是没有复制物理页,这个时候两个进程的虚拟地址和物理地址都是相同的,子进程和父进程使用同一份物理内存页,这时的页面标记时“只读”的。

当某个进程进行修改内存时,比如子进程进行修改内存操作,这个时候操作系统系统会把父进程的物理页拷贝一份给子进程,同时修改页表,子进程在新分配的物理页中进行修改,这个时候父子物理内存也就分开了。

因此,在子进程复制父进程的地址空间和页表后,父子进程都有独立的mm_struct 和 各级页表,且其值均相等。最关键的就是下表中红色的部分,所有可写的页表项均设置为不可写,当某个进程进行写访问时,就会触发缺页异常中断。

实现红色部分属性修改的函数调用流程如下:

copy_mm()
--> dup_mm()
--> dup_mmap()
--> copy_page_range()
--> copy_pud_range()
--> copy_pmd_range()
--> copy_pte_range()
--> copy_one_pte()
--> ptep_set_wrprotect()

static inline void
copy_one_pte(struct mm_struct *dst_mm, struct mm_struct *src_mm,
pte_t *dst_pte, pte_t *src_pte, struct vm_area_struct *vma,
unsigned long addr, int *rss)
{
...

/*
* If it's a COW mapping, write protect it both
* in the parent and the child
*/
if (is_cow_mapping(vm_flags)) {
ptep_set_wrprotect(src_mm, addr, src_pte);
pte = pte_wrprotect(pte);
}

...
}

从代码中可以看到,父子进程的页表项均设置成了写保护属性。

vfork 原理

在linux中还有一种创建进程的方式,那就是vfork。

除了不拷贝父进程的页表项外,vfork( ) 系统调用和 fork() 系统调用功能相同。子进程作为父进程的一个单独的线程在它的地址空间里运行,父进程被阻塞,直到子进程退出或者执行 execv()。

vfork 系统调用最终还是通过 do_fork 系统调用完成的,如下

int sys_vfork(long r10, long r11, long r12, long r13, long mof, long srp, struct pt_regs *regs)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, rdusp(), regs, 0, NULL, NULL);
}

在调用 do_fork 时 vfork 比 fork的 clone_flags 参数多了 CLONE_VFORK | CLONE_VM 标志,接下来通过这2个标志来进行分析。

  • vfork 在调用 copy_process 时,由于存在 CLONE_VM 标志,所以在 拷贝 copy_mm 时子进程并不对父进程 mm_struct 结构进行复制,而是子进程指向父进程的 mm_struct结构进行共享。
  • 在执行 do_fork 时,子进程的 vfork_done结构会指向一个特定的地址。
  • 子进程先开始执行后,父进程不是马上恢复执行,而是一直等待,直到子进程通过 vfork_done 指针向它发送信号。
  • 在调用 mm_release() 时,该函数用于进程退出内存地址空间,并且检查 vfork_done 是否为空,若不为空,则会向父进程发送信号。
  • 回到 do_fork,父进程醒来并返回。

若一切执行顺利,子进程在新的地址空间里运行而父进程也恢复了在原地址空间的运行。

由于子进程指向父进程的mm_struct结构,所以当子进程修改数据的时候父进程能够感知到。

创建线程

Linux 中实现线程的机制很特别。从内核的角度来看,并没有线程的概念。Linux 把所有的线程当做进程来实现。线程仅仅被视为一个与其他进程共享某些资源的进程。每个线程都拥有唯一隶属于自己的 task_struct。所以在内核中,它看起来像是一个普通的进程(只是线程和其他一些进程共享某些资源,比如地址空间等)。

在用户态中我们常用 pthread_create 来创建线程,而 pthread_create 在libc 库中调用 create_thread(), 最终调用 clone()。

__pthread_create_2_1 ()
-->ALLOCATE_STACK () 分配线程栈空间
--> create_thread ()
--> __clone2 ()

从上述调用过程可以知道,线程在创建时候,通过 libc 库创建了线程的栈,所以每个线程都有自己的私有栈。

在内核实现中,最终还是调用 do_fork。

int sys_clone(unsigned long clone_flags, unsigned long usp,
int __user *parent_tidp, void __user *child_threadptr,
int __user *child_tidp, int p6,
struct pt_regs *regs)
{

...

return do_fork(clone_flags, usp, regs, 0, parent_tidp, child_tidp);
}

线程的创建和普通进程的创建类似,只不过在调用 clone() 的时候需要传递一些参数标志来指明需要共享的资源。

const int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SYSVSEM
| CLONE_SIGHAND | CLONE_THREAD
| CLONE_SETTLS | CLONE_PARENT_SETTID
| CLONE_CHILD_CLEARTID
| 0);

从上面的标志位可以知道,线程共享了父进程的地址空间、打开的文件、文件系统信息、信号处理函数及被阻断的信号等信息。

exec调用

有关 exec 系列函数的调用,本文不再分析,可以参考文章 《Linux 可执行文件程序载入和执行过程》。

责任编辑:华轩 来源: 今日头条
相关推荐

2023-03-02 23:50:36

Linux进程管理

2011-01-11 13:47:27

Linux管理进程

2023-03-03 00:03:07

Linux进程管理

2021-06-15 08:02:55

Linux 进程管理

2021-04-22 07:47:46

Linux进程管理

2021-04-15 05:51:25

Linux

2009-10-28 10:01:57

2010-02-25 10:28:43

Linux进程管理

2023-03-05 15:28:39

CFSLinux进程

2021-05-12 07:50:02

CFS调度器Linux

2021-05-17 18:28:36

Linux CFS负载均衡

2009-09-16 08:43:51

linux进程线程

2014-08-01 15:38:37

Linux进程管理

2022-02-07 11:55:00

linux进程线程

2009-10-23 17:35:16

linux进程管理

2022-05-26 08:31:41

线程Java线程与进程

2021-07-06 21:30:06

Linux进程通信

2013-12-02 17:33:20

Linux进程多线程

2022-11-09 08:12:07

2021-03-17 21:34:44

Linux内存管理
点赞
收藏

51CTO技术栈公众号