1.fork流程

asmlinkage int sys_fork(struct pt_regs regs)
{
    return do_fork(SIGCHLD, regs.esp, &regs, 0, NULL, NULL);
}

这意味着在子进程终止后发送SIGCHLD信号通知父进程。最初,父子进程的栈地址相同(起始地址保存在IA-2系统的esp寄存器中)。但如果操作栈地址并写入数据,则COW机制会为每个进程分别创建一个栈副本。
struct pt_regs {
    long ebx; //可执行文件路径的指针(regs.ebx中
    long ecx; //命令行参数的指针(regs.ecx中)
    long edx; //环境变量的指针(regs.edx中)。
    long esi;
    long edi;
    long ebp;
    long eax;
    int  xds;
    int  xes;
    int  xfs;
    /* int  xgs; */s
    long orig_eax;
    long eip;
    int  xcs;
    long eflags;
    long esp;
    int  xss;
};

 

sys_clone的实现方式与上述调用相似,差别在于do_fork如下调用:
arch/x86/kernel/process_32.c 
asmlinkage int sys_clone(struct pt_regs regs) 
{ 
  unsigned long clone_flags; 
  unsigned long newsp; 
  int __user *parent_tidptr, *child_tidptr; 
  clone_flags = regs.ebx; 
  newsp = regs.ecx; 
  parent_tidptr = (int __user *)regs.edx; 
  child_tidptr = (int __user *)regs.edi; 
  if (!newsp) 
    newsp = regs.esp; 
  return do_fork(clone_flags, newsp, &regs, 0, parent_tidptr, child_tidptr); 
}

 

do_fork()的流程

调用copy_process()函数将fork()之前的信息复制一份给子进程。
如果是vfork的话,直接初始化完成处理信息。
用wake_up_new_task()函数将新创建的进程加入到调度器中,为其分配CPU。
如果是vfork(),父进程会等待子进程结束或者子进程调用exec函数族。

 

首先创建进程指针
复制当前的task_struct(下面会将到dup_task_struct()函数)
初始化互斥变量
检查进程数是否超过限制,由操作系统完成。
初始化一些变量
初始化进程数据结构,将进程状态设置为TASK_RUNNING
复制创建进程所需要的父进程的信息。如文件系统,信号,内存管理等
初始化子进程内核栈
最后设置子进程ID

 dup_task_struct后此时父子进程的task_struct实例只有一个成员不同:新进程分配了一个新的核心态栈,即task_struct->stack。通常栈和thread_info一同保存在一个联合中,thread_info保存了线程所需的所有特定于处理器的底层信息。

<sched.h> 
union thread_union { 
  struct thread_info thread_info; 
  unsigned long stack[THREAD_SIZE/sizeof(long)]; 
};

<asm-arch/thread_info.h> 
struct thread_info { 
  struct task_struct *task; /* 当前进程task_struct指针 */ 
  struct exec_domain *exec_domain; /* 执行区间 */
  unsigned long flags; /* 底层标志 */ 
  unsigned long status; /* 线程同步标志 */ 
   __u32 cpu; /* 当前CPU */ 
  int preempt_count; /* 0 => 可抢占, <0 => BUG */ 
  mm_segment_t addr_limit; /* 线程地址空间 */ 
  struct restart_block restart_block; 
}
flags可以保存各种特定于进程的标志,我们对其中两个特别感兴趣,如下所示。

 如果进程有待决信号则置位TIF_SIGPENDING。

 TIF_NEED_RESCHED表示该进程应该或想要调度器选择另一个进程替换本进程执行。

preempt_count实现内核抢占所需的一个计数器
restart_block用于实现信号机制

 

在dup_task_struct成功之后,内核会检查当前的特定用户在创建新进程之后,是否超出了允许的最大进程数目。如果资源限制无法防止进程建立,则调用接口函数sched_fork,以便使调度器有机会对新进程进行设置。
在内核版本2.6.23引入CFQ调度器之前,该过程要更加复杂,因为父进程的剩余时间片必须在父子进程之间分配。由于新的调度器不再需要时间片,现在简单多了。本质上,该例程会初始化一些统计字段,在多处理器系统上,如果有必要可能还会在各个CPU之间对可用的进程重新均衡一下
void sched_fork(struct task_struct *p, int clone_flags){
#ifdef CONFIG_SMP
    cpu = sched_balance_self(cpu, SD_BALANCE_FORK);
...
  p->prio = current->normal_prio;
...
#ifdef CONFIG_PREEMPT
    /* Want to start with kernel preemption disabled. */
    task_thread_info(p)->preempt_count = 1;
#endif
}

 

kernel/fork.c
p->pid = pid_nr(pid); p->tgid = p->pid; if (clone_flags & CLONE_THREAD) p->tgid = current->tgid;
在用之前描述的机制为进程分配一个新的pid实例之后,则保存在task_struct中。对于线程,线程组ID与分支进程(即调用fork/clone的进程)相同:

 

 if (clone_flags & (CLONE_PARENT|CLONE_THREAD))
      p->real_parent = current->real_parent;
 else
     p->real_parent = current;
 p->parent = p->real_parent;
对普通进程,父进程是分支进程。对于线程来说有些不同:由于线程被视为分支进程内部的第二(或第三、第四,等等)个执行序列,其父进程应是分支进程的父进程。
kernel/fork.c 
  p->group_leader = p; 
  if (clone_flags & CLONE_THREAD) { 
    p->group_leader = current->group_leader; 
    list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group); 
    ... 
  }
非线程的普通进程可通过设置CLONE_PARENT触发同样的行为。对线程来说还需要另一个校正,

即普通进程的线程组组长是进程本身。对线程来说,其组长是当前进程的组长:
 

 


kernel/fork.c
add_parent(p);新进程接下来必须通过children链表与父进程连接起来。
if (thread_group_leader(p)) {thread_group_leader只检查新进程的pid和tgid是否相同。倘若如此,则该进程是线程组的组
  if (clone_flags & CLONE_NEWPID) 
    p->nsproxy->pid_ns->child_reaper = p; 
  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)); 
} 
 新进程必须被加到当前进程组和会话。
  attach_pid(p, PIDTYPE_PID, pid); 
... 
  return p; 
}

 

 

用户线程库用于实现多线程功能的标志

用户空间线程库使用clone系统调用来生成新线程。该调用支持(上文讨论之外的)标志,对copy_process(及其调用的函数)具有某些特殊影响。在本节中,重点讲解用户线程库(尤其是NPTL)用于实现多线程功能的标志。
 CLONE_PARENT_SETTID将生成线程的PID复制到clone调用指定的用户空间中的某个地址(parent_tidptr,传递到clone的指针)①:
kernel/fork.c 
  if (clone_flags & CLONE_PARENT_SETTID) 
    put_user(nr, parent_tidptr);
复制操作在do_fork中执行,此时新线程的task_struct尚未初始化,copy操作尚未创建新线程的数据。
 CLONE_CHILD_SETTID首先会将另一个传递到clone的用户空间指针(child_tidptr)保存在新进程的task_struct中。
kernel/fork.c
p->set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? child_tidptr : NULL;
在新进程第一次执行时,内核会调用schedule_tail函数将当前PID复制到该地址。
复制代码
kernel/schedule.c
asmlinkage void schedule_tail(struct task_struct *prev){
...
  if (current->set_child_tid) 
    put_user(task_pid_vnr(current), current->set_child_tid); 
... 
}
复制代码
 CLONE_CHILD_CLEARTID首先会在copy_process中将用户空间指针child_tidptr保存在task_struct中,这次是另一个不同的成员。
kernel/fork.c
p->clear_child_tid = (clone_flags & CLONE_CHILD_CLEARTID) ? child_tidptr: NULL;
在进程终止时,② 将0写入clear_child_tid指定的地址。③
复制代码
kernel/fork.c
void mm_release(struct task_struct *tsk, struct mm_struct *mm)
{
  if (tsk->clear_child_tid && atomic_read(&mm->mm_users) > 1) { 
    u32 __user * tidptr = tsk->clear_child_tid; 
    tsk->clear_child_tid = NULL; 
    put_user(0, tidptr); 
    sys_futex(tidptr, FUTEX_WAKE, 1, NULL, NULL, 0); 
  } 
... 
}
复制代码
① put_user用于在内核地址空间和用户地址空间之间复制数据,将在第4章讨论。
② 或更精确地说,在进程终止过程中,使用mm_release自动释放其用于内存管理的数据结构时。
③ 条件mm->mm_users > 1意味着系统中至少有另一个进程在使用该内存管理数据结构。因此当前进程是一般意义上的一个线程,其地址空间来自另一个进程,且只有一个控制流。
此外,sys_futex,一个快速的用户空间互斥量,用于唤醒等待线程结束事件的进程。
上述标志可用于从用户空间检测内核中线程的产生和销毁。CLONE_CHILD_SETTID和CLONE_PARENT_SETTID用于检测线程的生成。CLONE_CHILD_CLEARTID用于在线程结束时从内核向用户空间传递信息。在多处理器系统上这些检测可以真正地并行执行。
posted @ 2022-04-25 22:25  while(true);;  阅读(109)  评论(0编辑  收藏  举报