我们都知道进程是linux内核中最为重要的一个抽象概念,那么我们平时在fork一个进程时,该进程究竟是咋么产生的呢?
本篇博文会浅谈一下在进程创建过程中扮演着重要角色的do_fork函数

1.内核如何来抽象一个进程

内核通过一个叫做task_struct的结构体来抽象一个进程

该结构体的定义(以内核2.6为例)在include/linux.sched.h中

截取部分task_struct如下

task_struct{
    volatile long state;    
    void *stack;
    atomic_t usage;
    unsigned int flags; 
    unsigned int ptrace;
    int lock_depth;     
    int prio, static_prio, normal_prio;
    unsigned int rt_priority;
    const struct sched_class *sched_class;
    struct sched_entity se;
    struct sched_rt_entity rt;
    unsigned char fpu_counter;
    struct list_head tasks;
    struct plist_node pushable_tasks;
    struct mm_struct *mm, *active_mm;
    pid_t pid;
    struct task_struct *real_parent; 
    struct task_struct *parent; 
    struct list_head children;  
    struct fs_struct *fs;
    struct files_struct *files;
    struct signal_struct *signal;
 

上述task_struct属性是我节选出的部分其结构体中的属性,我们从中可以大致了解到标识一个进程的属性大致会有该用以表示该进程所处的状态,进程的标志,以及进程是否被其他进程跟踪,进程锁的深度,进程的优先级,进程的pid,进程的父母,进程的孩子链表,进程所打开的文件描述符表,进程所处的文件系统,进程的信号。。。。等等一堆我们平时可能遇到的和进程相关的东西

2.do_fork简单分析

接触linuxC编程的人都知道,创建一个进程我们需要调用fork函数,fork其实又是调用了clone函数来实现的,而clone函数中最关键的函数就是do_fork函数。

在分析do_fork前我们脑海中可以大致想象一下,进程究竟是如何被创建出来的,假如让你来创建一个进程你会咋么做?
我们可以这样去分析,既然原来的进程被抽象成一个task_struct,那么新进程也是一个task_struct只不过它里面的一些属性会不同与原来的task_struct,那么创建一个新进程所要做的工作就是赋值一个与原来进程一样都的task_struct结构,然后然后将新进程的task_struct不同于原来task_struct的属性进行修改即可

do_fork定义在kernel/fork.c文件中
1.在分析该函数之前我们先来分析一下它的函数的各个参数
参数如下

 long do_fork(unsigned long clone_flags,
              unsigned long stack_start,
              struct pt_regs *regs,
              unsigned long stack_size,
              int __user *parent_tidptr,                                                                                               
              int __user *child_tidptr)

个参数具体含义如下

1.clone_flags:该参数是此函数中最重要的一个参数,该值中的每个位都代表对子进程task_struct中的每种属性的设置
2.stack_start:子进程用户态堆栈的开始地址
3.regs:当系统发生系统调用时,需从用户态切换到内核态,此结构体用来保存此时用户态进程中的通用寄存器中的值,并被存放在内核态堆栈中
4.stack_size:目前未被使用,通常设为0
5.parent_tidptr:父进程在用户态下pid的地址
6.child_tidptr:子进程在用户态下pid的地址

其中clone_flags的标志位宏定义如下:

 #define CSIGNAL     0x000000ff  /* signal mask to be sent at exit */
 #define CLONE_VM    0x00000100  /* set if VM shared between processes */
 #define CLONE_FS    0x00000200  /* set if fs info shared between processes */
 #define CLONE_FILES 0x00000400  /* set if open files shared between processes */
 #define CLONE_SIGHAND   0x00000800  /* set if signal handlers and blocked signals shared */
 #define CLONE_PTRACE    0x00002000  /* set if we want to let tracing continue on the child too */
 #define CLONE_VFORK 0x00004000  /* set if the parent wants the child to wake it up on mm_release */
 #define CLONE_PARENT    0x00008000  /* set if we want to have the same parent as the cloner */
 #define CLONE_THREAD    0x00010000  /* Same thread group? */                                                                       
  #define CLONE_NEWNS 0x00020000  /* New namespace group? */
 #define CLONE_SYSVSEM   0x00040000  /* share system V SEM_UNDO semantics */
 #define CLONE_SETTLS    0x00080000  /* create a new TLS for the child */
 #define CLONE_PARENT_SETTID 0x00100000  /* set the TID in the parent */
 #define CLONE_CHILD_CLEARTID    0x00200000  /* clear the TID in the child */
 #define CLONE_DETACHED      0x00400000  /* Unused, ignored */
 #define CLONE_UNTRACED      0x00800000  /* set if the tracing process can't force CLONE_PTRACE on this clone */
 #define CLONE_CHILD_SETTID  0x01000000  /* set the TID in the child */
 #define CLONE_STOPPED       0x02000000  /* Start in stopped state */
 #define CLONE_NEWUTS        0x04000000  /* New utsname group? */
 #define CLONE_NEWIPC        0x08000000  /* New ipcs */
 #define CLONE_NEWUSER       0x10000000  /* New user namespace */
 #define CLONE_NEWPID        0x20000000  /* New pid namespace */
 #define CLONE_NEWNET        0x40000000  /* New network namespace */
 #define CLONE_IO        0x80000000  /* Clone io context */
 

举个简单的例子当我们的参数中设置了CLONE_VM这个宏,那么就以为这我们新创建的进程和其父进程要共享VM,当我们设置了CLONE_FILES时意味这父子进程之间共享打开的文件描述符

do_fork开始执行后首先做的就是为子进程定义一个新的task_struct指针

struct task_struct *p;

在下来会检查一些clone_flags所不允许的位组合
例如:

if (clone_flags & CLONE_NEWUSER) {
         if (clone_flags & CLONE_THREAD)
             return -EINVAL;

上述中不允许同时既设置了CLONE_NEWUSER标志,还设置CLONE_THREAD标志,这样就会产生错误

类似上面当一系列的安全检查完毕之后,copy_process函数就登场了

copy_process函数工作流程具体如下:
1)调用dup_task_struct函数为新的进程创建一个内核栈,thread_info结构和task_struct等,当然此时的值都是和父进程完全一样的

dup_task_struct函数定义如下

static struct task_struct *dup_task_struct(struct task_struct *orig)
    struct task_struct *tsk;
    struct thread_info *ti;
    unsigned long *stackend;
    int err;
    prepare_to_copy(orig);
    //为tsk分配内存空间
    tsk = alloc_task_struct();
    if (!tsk)
        return NULL;
    //为ti分配内存空间
    ti = alloc_thread_info(tsk);
    if (!ti) {
        free_task_struct(tsk);
        return NULL;
    赋值orig属性给新的tsk
    err = arch_dup_task_struct(tsk, orig);
    if (err)
        goto out;
    tsk->stack = ti;
    //初始化进程缓存脏数据
    err = prop_local_init_single(&tsk->dirties);
    if (err)
        goto out;
    //设置线程栈空间
    setup_thread_stack(tsk, orig);
    stackend = end_of_stack(tsk);
    *stackend = STACK_END_MAGIC;    /* for overflow detection */
#ifdef CONFIG_CC_STACKPROTECTOR
    tsk->stack_canary = get_random_int();
#endif
    /* One for us, one for whoever does the "release_task()" (usually parent) */
    atomic_set(&tsk->usage,2);
    atomic_set(&tsk->fs_excl, 0);
#ifdef CONFIG_BLK_DEV_IO_TRACE
    tsk->btrace_seq = 0;
#endif
    tsk->splice_pipe = NULL;
    account_kernel_stack(ti, 1);
    return tsk;
out:
    free_thread_info(ti);
    free_task_struct(tsk);
    return NULL;

2)检查并确保新创建该子进程后,当前用户所拥有的进程数没有超出给它分配的资源限制
代码如下

if (atomic_read(&p->real_cred->user->processes) >=                                                                      
             p->signal->rlim[RLIMIT_NPROC].rlim_cur) {
         if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) &&
             p->real_cred->user != INIT_USER)
             goto bad_fork_free;

3)子进程着手使自己与父进程区别开来,从父进程那继承过来的许多属性都要被清0或设置一个初始值,但task_struct中的大多数数据还是未被修改

部分代码如下

     spin_lock_init(&p->alloc_lock);
     init_sigpending(&p->pending);
     p->utime = cputime_zero;
     p->stime = cputime_zero;
     p->gtime = cputime_zero;
     p->utimescaled = cputime_zero;
     p->stimescaled = cputime_zero;
     p->prev_utime = cputime_zero;
     p->prev_stime = cputime_zero;
     p->default_timer_slack_ns = current->timer_slack_ns;
    task_io_accounting_init(&p->ioac);
     acct_clear_integrals(p);
     posix_cpu_timers_init(p);
     p->lock_depth = -1;     /* -1 = no lock */
     do_posix_clock_monotonic_gettime(&p->start_time);
     p->real_start_time = p->start_time;
     monotonic_to_bootbased(&p->real_start_time);
     p->io_context = NULL;
     p->audit_context = NULL;
#ifdef CONFIG_TRACE_IRQFLAGS
     p->irq_events = 0;
 #ifdef __ARCH_WANT_INTERRUPTS_ON_CTXSW
     p->hardirqs_enabled = 1;
 #else
     p->hardirqs_enabled = 0;
 #endif
     p->hardirq_enable_ip = 0;
     p->hardirq_enable_event = 0;
     p->hardirq_disable_ip = _THIS_IP_;
     p->hardirq_disable_event = 0;
     p->softirqs_enabled = 1;
     p->softirq_enable_ip = _THIS_IP_;
     p->softirq_enable_event = 0;
     p->softirq_disable_ip = 0;
     p->softirq_disable_event = 0;
     p->hardirq_context = 0;
     p->softirq_context = 0;
 #endif
 #ifdef CONFIG_LOCKDEP
     p->lockdep_depth = 0; /* no locks held yet */
     p->curr_chain_key = 0;
     p->lockdep_recursion = 0;
 #endif
 #ifdef CONFIG_DEBUG_MUTEXES
     p->blocked_on = NULL; /* not blocked yet */
 #endif
     p->bts = NULL;

4)给子进程分配一个CPU

代码如下:

sched_fork(p, clone_flags);

5)
接着就是子进程拷贝父进程的一些资源,具体如下

调用copy_files函数拷贝父进程打开的文件描述符

748 static int copy_files(unsigned long clone_flags, struct task_struct * tsk)
      struct files_struct *oldf, *newf;
      int error = 0;                                                                                                          
      oldf = current->files;
      if (!oldf)
          goto out;
      if (clone_flags & CLONE_FILES) {
          //增加文件描述符的引用计数
          atomic_inc(&oldf->count);
          goto out;
      //复制文件描述符
      newf = dup_fd(oldf, &error);
      if (!newf)
          goto out;
      tsk->files = newf;
      error = 0;
  out:
      return error;
 

调用copy_fs继承父进程所属的文件系统

static int copy_fs(unsigned long clone_flags, struct task_struct *tsk)
      struct fs_struct *fs = current->fs;
      if (clone_flags & CLONE_FS) {
          /* tsk->fs is already what we want */
          write_lock(&fs->lock);
          if (fs->in_exec) {
              write_unlock(&fs->lock);
              return -EAGAIN;
          //将该文件系统的用户数加1
          fs->users++;
          write_unlock(&fs->lock);
          return 0;
      //将加1后的fs拷贝给进程的fs
      tsk->fs = copy_fs_struct(fs);
      if (!tsk->fs)
          return -ENOMEM;
      return 0;
 

调用copy_signal函数拷贝并设置新的signal_struct

signal_struct包含了大量的进程运行的信息

调用copy_mm函数处理与新进程的内存问题

 static int copy_mm(unsigned long clone_flags, struct task_struct * tsk)
      struct mm_struct * mm, *oldmm;
      int retval;
      tsk->min_flt = tsk->maj_flt = 0;
      tsk->nvcsw = tsk->nivcsw = 0;
  #ifdef CONFIG_DETECT_HUNG_TASK
      tsk->last_switch_count = tsk->nvcsw + tsk->nivcsw;
  #endif
      tsk->mm = NULL;
      tsk->active_mm = NULL;
       * Are we cloning a kernel thread?
       * We need to steal a active VM for that..
      oldmm = current->mm;
      if (!oldmm)
          return 0;
      //如果是vfork则共享内存,此处为增加引用计数
      if (clone_flags & CLONE_VM) {
          atomic_inc(&oldmm->mm_users);
          mm = oldmm;
          goto good_mm;
      retval = -ENOMEM;
      //新分配一块内存,并将父进程内存中的数据copy到新内存中,说明2.6内核并没有fork的写时拷贝
      mm = dup_mm(tsk);
      if (!mm)
          goto fail_nomem;
  good_mm:
      /* Initializing for Swap token stuff */
      mm->token_priority = 0;
      mm->last_interval = 0;
      tsk->mm = mm;
      tsk->active_mm = mm;
      return 0;
  fail_nomem:
      return retval;
 

调用copy_io函数拷贝父进程的I/O情况

static int copy_io(unsigned long clone_flags, struct task_struct *tsk)
  #ifdef CONFIG_BLOCK
      struct io_context *ioc = current->io_context;
      if (!ioc)
          return 0;
       * Share io context with parent, if CLONE_IO is set
      if (clone_flags & CLONE_IO) {
          tsk->io_context = ioc_task_link(ioc);
          if (unlikely(!tsk->io_context))
              return -ENOMEM;
      } else if (ioprio_valid(ioc->ioprio)) {
          tsk->io_context = alloc_io_context(GFP_KERNEL, -1);
          if (unlikely(!tsk->io_context))
              return -ENOMEM;
          tsk->io_context->ioprio = ioc->ioprio;
  #endif
      return 0;
 

还有调用copy_namespaces 和 copy_thread等,这里就不在赘述

6)调用alloc_pid为新进程分配一个pid

pid = alloc_pid(p->nsproxy->pid_ns);  

7)copy_process做一些收尾工作,并返回新进程的task_struct指针

此时再次回到了do_fork,新创建的子进程被唤醒,并让其先投入运行

         if (unlikely(clone_flags & CLONE_STOPPED)) {
              * We'll start up with an immediate SIGSTOP.
             sigaddset(&p->pending.signal, SIGSTOP);
             set_tsk_thread_flag(p, TIF_SIGPENDING);
             __set_task_state(p, TASK_STOPPED);
         } else {
             //换新新的进程
             wake_up_new_task(p, clone_flags);                                                                               

到这里本篇关于do_fork的博文也就基本结束了

关于进程创建的源码理解,我感觉主要抓住俩点即可。第一进程被内核抽象成了啥?它的数据结构是咋样的(task_struct)这点我们必须有所认识,第二创建进程最主要的其实就是拷贝父进程的task_struct里的属性,但是关键点是拷贝哪些,哪些又是子进程和父进程所不同的,很简单我们只需要把握住进程创建函数里的clone_flags参数就可以知道咋么拷贝了。

我们都知道进程是linux内核中最为重要的一个抽象概念,那么我们平时在fork一个进程时,该进程究竟是咋么产生的呢? 本篇博文会浅谈一下在进程创建过程中扮演着重要角色的do_fork函数1.内核如何来抽象一个进程内核通过一个叫做task_struct的结构体来抽象一个进程该结构体的定义(以内核2.6为例)在include/linux.sched.h中截取部分task_struct如下task_st
在最新的版本的POSIX标准中,定义了进程创建和终止的操作,进程创建包括fork()和execve(),进程终止包括wait(),waitpid(),kill()以及exit()。Linux系统为了提高效率,把POSIX标准的fork()扩展为vfork和clone。 前面一章我们学习了用GCC将一个最简单的程序(如hello world程序)编译成ELF文件,在shell提示符下输入该可执行文件并且按回车后,这个程序就开始执行了。起始这里shell会调用fork()来创建一个新进程,然后调用execve(
进程是什么?  进程是Linux系统抽象出来的,表示正在执行程序的实时结果。进程不等于程序,程序是静态地存储在磁盘上,而进程活动于操作系统中。多个进程有可能在执行同一份程序代码。进程除了包括程序代码外,还包括其他资,像打开的文件、挂起的信号、内核数据、处理器状态、一个或多个具有内存映射的内存地址空间、一个或多个执行线程等。  线程是进程中的活动对象,一个进程对应多个线程。线程共享进程的部分资,像代码段、数据段、堆、当前目录、用户ID、组ID、打开的文件描述符等,也独有资,像栈、程序计数器、程序运行时用
do_fork()分析 从上文可得知, fork、vfork和clone三个系统调用所对应的系统调用服务例程均调用了do_fork()。只不过在调用时所传递的参数有所不同,而参数的不同正好导致了子进程与父进程之间对资的共享程度不同。因此,分析do_fork()成为我们的首要任务。 在进入do_fork函数进行
中断和异常向量 每个中断和异常是由0-255之间的一个数来标识。因为一些为重的原因,Inter把这8位无符号整数叫做一个向量。不可屏蔽中断的向量和异常的向量是固定的,而可屏蔽中断的向量可以通过对中断控制器的编程来改变。 Linux利用了下列向量: 0-31的向量对应于异常和不可屏蔽中断 32-47的向量被分配给可屏蔽中断,即由IRQ引起的中断。 生于的48-255的向量用来标识软中断。Linux...
long do_fork(unsigned long clone_flags, unsigned long stack_start, unsigned long stack_size, int __user *parent_tidptr, int __user *child_tidpt...
一般进程的创建分为两步:fork() + exec() fork()通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅仅在于PID、PPID和某些资和统计量。 exec()函数负责读取可执行文件并将其载入地址空间开始运行。 fork(),vfork(),__clone()根据各自需要的参数标志去调用clone()。然后由clone()去调用do_fork()。
clone do_forkLinux内核中的一个函数,用于创建一个新的进程,并将其设置为当前进程的孩子进程。下面是对clone do_fork的解释: 在计算机操作系统中,进程是正在运行的程序的实例。通过创建新的进程,可以在同一个程序中同时运行多个独立的任务。 clone do_fork函数是实现进程的克隆功能的关键函数。当调用该函数时,它将创建一个新的进程,称为子进程,并将其设置为与当前进程(父进程)共享内存和其他资。 克隆的进程在许多方面与父进程相同,包括代码,数据和堆栈。然而,子进程有自己的程序计数器和寄存器集合,使其能够独立地运行。 clone do_fork函数的工作原理如下: 1. 首先,将当前进程的状态保存到一个数据结构中,以便稍后以原始状态恢复父进程。 2. 创建一个新的进程,并将其标记为子进程。 3. 共享父进程的内存和其他资,以确保子进程可以访问相同的代码和数据。 4. 设置子进程的程序计数器和寄存器集合以独立运行。 5. 使用资调度算法来决定孩子进程何时运行。 通过使用clone do_fork函数,可以实现进程的多任务并发执行。这对于同时执行多个任务或利用多核处理器的计算机非常有用。