• 正文
  • 相关推荐
申请入驻 产业图谱

【Linux内核设计思想】四、进程管理(二)

01/06 15:14
282
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

进程创建

进程的产生与替换

在其它操作系统中,都会提供一种产生(spawn)进程的机制,首先在新的地址空间里创建进程,然后读入可执行文件,最后开始执行。在Unix/Linux中,进程的产生是分两步进行的,也就是我们使用的系统调用fork()函数和exec()函数族。首先,父进程通过调用fork()拷贝自己来创建一个子进程(fork一次调用两次返回),子进程和父进程的区别仅仅在于PID(每个进程唯一)、PPID(父进程ID)和一些没必要继承的资源(比如挂起的信号等)。最后,exec()函数族读取可执行文件并将其载入地址空间运行。

我们在使用fork()系统调用之后,创建出来的子进程是对父进程的复制,也就是说子进程和父进程执行的是相同的程序,虽然说父子进程可能执行的是不同的代码分支(if else语句),但是程序流程是一样。我们要想在新创建的子进程中执行其他程序,需要调用一种exec函数(exec开头的函数总共有6种,统称exec函数族)来拉起一个新的进程。当进程调用一种exec函数的时候,该进程的用户空间代码和数据全部被新程序替换掉,从新程序的启动例程开始执行。需要注意的是,调用exec并不会创建新进程,而是一种进程替换,所以调用exec前后,进程本身的ID不会改变。

调用exec函数的时候,会把当前进程的 .text 和 .data 替换为所要加载的程序的 .text 和 .data ,然后让进程从新的进程的 .text 段的第一条指令开始执行,但是进程ID不变,也就是说壳子没变,但壳子里的东西变了。

exec函数族具有如下的调用关系

fork一次创建两次返回

fork系统调用接口

包含头文件<sys/types.h>和<unistd.h>

函数功能:创建一个子进程

函数原型:pid_t fork(void)

函数参数:无

函数返回值:如果成功创建一个子进程,对父进程来说返回子进程ID,对于子进程来说返回0,一次创建两次返回;创建失败返回-1;

复制一个进程映像fork

使用fork函数得到的子进程从父进程继承了整个进程的地址空间,包括:进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设置、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端等;

子进程与父进程的区别在于:

(1)父进程设置的锁,子进程不继承;

(2)各自的进程ID和父进程的ID不同;

(3)子进程的未决告警被清除;

(4)子进程的未决信号集设置为空集;

一次创建两次返回:fork创建一个进程后,会从用户态陷入到内核态,创建的进程会进入就绪队列,进程的内存空间拷贝,调度都是在内核中进行的(内核态),fork一次调用两次返回的本质是,父子进程都在各自的内存空间中返回(由内核态返回到用户态),因为每个进程都有自己的内存空间。父进程返回子进程id,是因为一个父进程可以创建多个子进程,这样可以方便父进程使用子进程id,在父进程fork多个子进程的时候,可以使用返回的子进程的id来区分并控制各个子进程。而子进程只有一个父进程,直接使用获取父进程id的函数getppid()就可以获取父进程id,所以不需要返回父进程id,而是返回0。并且子进程是在父进程中创建的,也就是说在父进程中调用fork函数,所以在父进程中返回子进程id。

子进程对父进程的拷贝:子进程拷贝了父进程的代码段,堆栈段(内存四区)、数据段、进程控制块PCB(因为PCB记录了进程的状态,操作系统要靠PCB来控制进程)。子进程不会拷贝父进程的锁,父子进程共享文件。父子进程的变量是拷贝的关系,并不是同一个变量,因为父子进程有各自的内存空间。

fork返回后:父进程先运行还是子进程先运行?由Linux内核决定。

fork创建失败返回-1:每个进程空间都会有一个变量errno用来记录错误信息。

子进程开始的位置:子进程开始的代码是从fork语句之后开始的,在fork之后,父子进程同时进行,也就是说子进程的运行是在fork语句后开始的,但是他拷贝了父进程在fork之前的内存四区(父进程fork之前对内存四区的赋值会被子进程拷贝,所以,子进程只需要拷贝内存四区就可以了,而不需要再去执行那些代码)。

写时拷贝(copy-on-write)

如果在fork()系统调用时复制父进程的所有资源会有一个问题,如果新进程立即执行一个新的映像,那么所有的拷贝都没有意义了,并且拷贝的数据也许并不共享,这样的话效率将会非常低下。Linux的fork()使用写时拷贝页来实现。写时拷贝是一种可以推迟甚至免除拷贝数据的技术,内核并不会复制整个进程地址空间,而是让父子进程共享同一份拷贝,只有在写入的时候数据才会被拷贝,让每个进程有自己的拷贝。这也就是我们说的“读时共享,写时复制”,只有写入时才会拷贝,否则以只读方式共享。这就使得地址空间上的页拷贝被推迟到实际写入的时候,如果页根本不会被写入,那么就无需复制了,比如说fork()之后立马exec替换。这样,fork()的实际开销就是复制父进程的页表并给子进程创建进程描述符。这也给了我们优化代码的思路,在进程创建后立马调用exec来运行一个可执行文件,这样就避免了拷贝大量无用数据,提升执行速度。

下面深入分析一下“读时共享,写时复制”机制。

执行fork()函数后,子进程与父进程有相同的全局变量、.data段、.text段、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式等。不同之处在于,进程自己的ID、父进程ID、fork()函数返回值、进程运行时间(父进程在fork之前就已经运行了,而子进程在fork之后才开始运行)、定时器、未决信号集等不同。但是,子进程并不是直接把父进程0到3G的用户空间全部复制,而是遵循一种“读时共享、写时复制”这样的原则,这样无论是子进程执行父进程的逻辑,还是执行自己的逻辑都能节省内存开销。也就是说,父子进程的虚拟地址空间中,比如说数据段,它们都是指向同一块物理地址空间的,如果子进程只是读取该空间,那么就没必要复制这块物理内存,即读时共享,如果子进程要修改这块物理空间,那么将会复制一块物理空间然后修改复制的空间,即写时复制。这里要注意,即便是全局数据,也遵循读时共享写时复制的原则,也就是说全局变量在父子进程之间也不是共享的。

fork()与vfork()

Linux内核通过clone()系统调用来实现fork(),clone()的一系列参数标志指明了父子进程需要共享的资源等。而fork()、vfork()、__clone()等库函数根据自己需要的参数去调用clone()。最终,clone()再去调用do_fork()。下面代码见<kernelfork.c>、<archi386kernelprocess.c>

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

/*
* Ok, this is the main fork-routine. It copies the system process
* information (task[nr]) and sets up the necessary registers. It also
* copies the data segment in its entirety. The "stack_start" and
* "stack_top" arguments are simply passed along to the platform
* specific copy_thread() routine. Most platforms ignore stack_top.
* For an example that's using stack_top, see
* arch/ia64/kernel/process.c.
*/
int do_fork(unsigned long clone_flags, unsigned long stack_start,
       struct pt_regs *regs, unsigned long stack_size)
{
   int retval = -ENOMEM;
   struct task_struct *p;
   DECLARE_MUTEX_LOCKED(sem);

   if (clone_flags & CLONE_PID) {
       /* This is only allowed from the boot up thread */
       if (current->pid)
           return -EPERM;
  }

   current->vfork_sem = &sem;

   p = alloc_task_struct();
   if (!p)
       goto fork_out;

   *p = *current;

   retval = -EAGAIN;
   if (atomic_read(&p->user->processes) >= p->rlim[RLIMIT_NPROC].rlim_cur)
       goto bad_fork_free;
   atomic_inc(&p->user->__count);
   atomic_inc(&p->user->processes);

   /*
    * Counter increases are protected by
    * the kernel lock so nr_threads can't
    * increase under us (but it may decrease).
    */
   if (nr_threads >= max_threads)
       goto bad_fork_cleanup_count;

   get_exec_domain(p->exec_domain);

   if (p->binfmt && p->binfmt->module)
       __MOD_INC_USE_COUNT(p->binfmt->module);

   p->did_exec = 0;
   p->swappable = 0;
   p->state = TASK_UNINTERRUPTIBLE;

   copy_flags(clone_flags, p);
   p->pid = get_pid(clone_flags);

   p->run_list.next = NULL;
   p->run_list.prev = NULL;

   if ((clone_flags & CLONE_VFORK) || !(clone_flags & CLONE_PARENT)) {
       p->p_opptr = current;
       if (!(p->ptrace & PT_PTRACED))
           p->p_pptr = current;
  }
   p->p_cptr = NULL;
   init_waitqueue_head(&p->wait_chldexit);
   p->vfork_sem = NULL;
   spin_lock_init(&p->alloc_lock);

   p->sigpending = 0;
   init_sigpending(&p->pending);

   p->it_real_value = p->it_virt_value = p->it_prof_value = 0;
   p->it_real_incr = p->it_virt_incr = p->it_prof_incr = 0;
   init_timer(&p->real_timer);
   p->real_timer.data = (unsigned long) p;

   p->leader = 0;        /* session leadership doesn't inherit */
   p->tty_old_pgrp = 0;
   p->times.tms_utime = p->times.tms_stime = 0;
   p->times.tms_cutime = p->times.tms_cstime = 0;
#ifdef CONFIG_SMP
  {
       int i;
       p->has_cpu = 0;
       p->processor = current->processor;
       /* ?? should we just memset this ?? */
       for(i = 0; i < smp_num_cpus; i++)
           p->per_cpu_utime[i] = p->per_cpu_stime[i] = 0;
       spin_lock_init(&p->sigmask_lock);
  }
#endif
   p->lock_depth = -1;        /* -1 = no lock */
   p->start_time = jiffies;

   retval = -ENOMEM;
   /* copy all the process information */
   if (copy_files(clone_flags, p))
       goto bad_fork_cleanup;
   if (copy_fs(clone_flags, p))
       goto bad_fork_cleanup_files;
   if (copy_sighand(clone_flags, p))
       goto bad_fork_cleanup_fs;
   if (copy_mm(clone_flags, p))
       goto bad_fork_cleanup_sighand;
   retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);
   if (retval)
       goto bad_fork_cleanup_sighand;
   p->semundo = NULL;

   /* Our parent execution domain becomes current domain
      These must match for thread signalling to apply */

   p->parent_exec_id = p->self_exec_id;

   /* ok, now we should be set up.. */
   p->swappable = 1;
   p->exit_signal = clone_flags & CSIGNAL;
   p->pdeath_signal = 0;

   /*
    * "share" dynamic priority between parent and child, thus the
    * total amount of dynamic priorities in the system doesnt change,
    * more scheduling fairness. This is only important in the first
    * timeslice, on the long run the scheduling behaviour is unchanged.
    */
   p->counter = (current->counter + 1) >> 1;
   current->counter >>= 1;
   if (!current->counter)
       current->need_resched = 1;

   /*
    * Ok, add it to the run-queues and make it
    * visible to the rest of the system.
    *
    * Let it rip!
    */
   retval = p->pid;
   p->tgid = retval;
   INIT_LIST_HEAD(&p->thread_group);
   write_lock_irq(&tasklist_lock);
   if (clone_flags & CLONE_THREAD) {
       p->tgid = current->tgid;
       list_add(&p->thread_group, &current->thread_group);
  }
   SET_LINKS(p);
   hash_pid(p);
   nr_threads++;
   write_unlock_irq(&tasklist_lock);

   if (p->ptrace & PT_PTRACED)
       send_sig(SIGSTOP, p, 1);

   wake_up_process(p);        /* do this last */
   ++total_forks;

fork_out:
   if ((clone_flags & CLONE_VFORK) && (retval > 0))
       down(&sem);
   return retval;

bad_fork_cleanup_sighand:
   exit_sighand(p);
bad_fork_cleanup_fs:
   exit_fs(p); /* blocking */
bad_fork_cleanup_files:
   exit_files(p); /* blocking */
bad_fork_cleanup:
   put_exec_domain(p->exec_domain);
   if (p->binfmt && p->binfmt->module)
       __MOD_DEC_USE_COUNT(p->binfmt->module);
bad_fork_cleanup_count:
   atomic_dec(&p->user->processes);
   free_uid(p->user);
bad_fork_free:
   free_task_struct(p);
   goto fork_out;
}

/*
* Save a segment.
*/
#define savesegment(seg,value) 
asm volatile("movl %%" #seg ",%0":"=m" (*(int *)&(value)))

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;

// 新进程的内核栈
childregs = ((struct pt_regs *) (THREAD_SIZE + (unsigned long) p)) - 1;
struct_cpy(childregs, regs);
childregs->eax = 0;
childregs->esp = esp;

p->thread.esp = (unsigned long) childregs;
p->thread.esp0 = (unsigned long) (childregs+1);

p->thread.eip = (unsigned long) ret_from_fork;

savesegment(fs,p->thread.fs);
savesegment(gs,p->thread.gs);

unlazy_fpu(current);
struct_cpy(&p->thread.i387, &current->thread.i387);

return 0;
}

do_fork()完成了创建进程的工作,它会调用copy_process()函数,然后运行进程。copy_process()主要完成了下面的工作

调用dup_task_struct()为新进程创建内核栈、thread_info结构和task_struct,并且这些创建的值和父进程完全一样,且父子进程的进程描述符也完全一样。

检查新创建的子进程,以及当前用户所拥有的进程数没有超出资源限制。

将子进程与父进程区分开,子进程描述符内的成员清零或设为初始值。进程描述符成员值主要用于统计信息,其中大多数数据都是共享的。

将子进程状态设置为TASK_UNINTERRUPTIBLE以保证子进程不会被运行。

copy_process()调用copy_flags()来更新task_struct的flags成员,PF_SUPERPRIV标志清零(该标志表明进程是否拥有超级用户权限),PF_FORKENOEXEC标志被设置(表示进程还未调用exec)。

调用get_pid()为新进程获取一个有效PID。

根据传递给clone()的参数标志,copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间、命名空间等。一般这些资源会被给定进程的所有线程共享,否则,这些资源对每个进程是不同的,需要拷贝。

父进程和子进程平分剩余时间片。

copy_process()做一些末尾处理并返回指向子进程的指针。

copy_process()函数执行完后返回do_fork()函数,如果copy_process()执行成功,新创建的子进程将会被唤醒并投入运行。内核一般会选择子进程首先执行,因为子进程一般会马上调用exec函数,这样就避免了写时拷贝的额外开销(如果父进程先运行,可能会向地址空间写入)。

vfork()系统调用和fork()功能相同,区别在于vfork()不拷贝父进程的页表项,子进程作为父进程的一个单独线程在它的地址空间运行,父进程被阻塞,直到子进程退出或者执行exec函数,子进程不能向地址空间写入。(由于fork()已经引入了写时拷贝页机制并且明确子进程先执行,所以现在vfork()的优势仅限于不拷贝父进程的页表项,如果fork()支持了写时拷贝页表项,那么vfork()就没有任何优势了)。

vfork()系统调用是通过向clone()系统调用传递一个特殊标志来实现的。

调用copy_process()时,task_struct的vfork_done成员被设置为NULL。

执行do_fork()时,雨果给定了特殊标志,那么vfork_done成员会指向一个特殊地址。

子进程开始执行后,父进程不会马上恢复执行,而是一直等待,直到子进程通过vfork_done指针向它发送信号。

调用mm_release()时,该函数用于进程退出内存地址空间,并检查vfork_done成员是否为空,如果不为空则向父进程发送信号。

返回do_fork(),父进程被唤醒并返回。

执行成功后,子进程在新的地址空间运行,父进程在原地址空间运行。

Linux线程的实现以及内核线程

线程机制提供了一组线程在同一程序中共享内存地址空间的运行方式,这些线程共享打开的文件和其它资源。并且线程机制支持并发程序设计(concurrent programming),在多处理器系统中,它保证了真正的并行处理(paralleling)。

实际上,在Linux内核的角度并没有线程这一概念。Linux内核把线程当作进程来实现,Linux内核并没有专门的调度算法或者数据结构来表征线程,它不区分进程与线程,仅仅是将线程看做与其它进程共享某些资源的进程,同样每个线程也都拥有自己唯一的task_struct结构。

不同的是,像Windows或其它一些操作系统在内核中提供了专门支持线程的机制,在这些系统中,通常把线程称为轻量级进程(lightweight process),相较于重量级进程,线程被抽象为一种耗费资源较少且运行迅速的执行单元。对Linux来说,线程只是一种进程间共享资源的手段,因为Linux中的进程本身就非常轻量级了。在Windows中,通常会在一个进程描述符中包含指向每个线程的指针,它描述了这些线程共享的资源,然后每个线程还有自己的描述符来描述自己独占的资源。而Linux内核中,每个线程都会分配属于自己的task_struct结构,在创建线程(进程)时指定共享资源就可以了。

在Linux中,创建线程和创建进程的唯一区别就是,要向clone()系统调用传递一些参数来指明要共享的资源。

clone(CLONE_VM | CLONE_FILES, 0);

传递给clone()的参数标志决定了新创建进程的行为方式以及父子进程之间共享的资源种类,下面是clone()的参数标志及作用描述。

前面说过,fork()和vfork()都是通过clone()实现的,它们的实现如下

/* fork()实现 */
clone(SIGCHLD, 0);

/* vfork()实现 */
clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0);

内核经常要在后台执行一些操作,这些操作是通过内核线程(kernel thread)来完成的。内核线程是一种独立运行在内核空间的标准进程,它和普通进程的区别在于内核进程没有独立的地址空间(内核线程没有自己的虚拟空间结构 struct mm,mm指针被设置为NULL),且内核进程只在内核空间运行,不会切换到用户空间。内核进程可以被调度,也可以被抢占。像pdflush、ksoftirqd这些任务在系统启动时都是由内核线程启动的。

和进程一样,内核线程也是由其它内核线程创建的,创建方法如下

int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags)
{
long retval, d0;

__asm__ __volatile__(
"movl %%esp,%%esint"
"int $0x80nt"/* Linux/i386 system call */
"cmpl %%esp,%%esint"/* child or parent? */
"je 1fnt"/* parent - jump */
/* Load the argument into eax, and push it. That way, it does
 * not matter whether the called function is compiled with
 * -mregparm or not. */
"movl %4,%%eaxnt"
"pushl %%eaxnt"
"call *%5nt"/* call fn */
"movl %3,%0nt"/* exit */
"int $0x80n"
"1:t"
:"=&a" (retval), "=&S" (d0)
:"0" (__NR_clone), "i" (__NR_exit),
 "r" (arg), "r" (fn),
 "b" (flags | CLONE_VM)
: "memory");
return retval;
}

该函数是使用嵌入汇编来实现的,主要过程是通过调用 _clone()函数来创建一个新的进程,而创建进程是通过传入 CLONE_VM 标志来指定进程借用其他进程的虚拟内存空间结构。

在这里,d0 局部变量的作用是为了在创建内核线程时保证 struct pt_regs 结构的完整,因为创建内核线程是在内核态进行的,所以在内核态调用系统调用是不会压入 ss 和 esp 寄存器的,这样就会导致系统调用的 struct pt_regs 参数信息不完整,所以 kernel_thread() 函数定义了一个 d0 局部变量是为了补充没压栈的 ss 和 esp 的。

在函数接口int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags)中,新任务的创建也是通过向clone()系统调用传递特定参数来实现的。该函数返回时,父线程退出并返回一个指向子线程task_struct的指针,子线程开始运行函数指针fn指向的函数,arg则是运行时需要的参数。

一般来说,内核线程创建后,就会永远执行创建时传递给它的函数,除非系统重启。该函数通常由一个循环实现,在需要使用时,该内核线程会被唤醒并执行,执行完任务则自行休眠。

进程的终结

进程终结时,内核必须释放进程所占有的所有资源,并把该进程的终结告知其父进程。进程资源的析构通常是通过调用exit()函数来实现的,进程的终结实际上有三种情况:

一是显式调用了exit()系统调用;

二是隐式调用exit()系统调用,比如C编译器会默认在main()函数的返回点调用exit()系统调用;

三是进程遇到了既无法处理也无法忽略的信号或异常,导致进程被动终结。

进程终结的这些工作都是由do_exit()来完成的:

首先,将task_struct中的标志设置为PF_EXITING;

调用del_timer_sync()删除内核定时器,根据返回结果,确保没有定时器在排队也没有定时器处理程序在运行;

如果BSD的进程记账功能是开启的,do_exit()调用acct_process()来输出记账信息;

调用_exit_mm()函数放弃进程占用的mm_struct,如果没有别的进程使用它且没有被共享,那么就析构该资源;

调用exit__sem()函数,如果此时进程排队等待IPC信号,那么则离开队列;

调用_exit_files()、_exit_fs()、exit_namespace()、exit_sighand(),分别递减文件描述符、文件系统数据、进程名字空间和信号处理函数的引用计数,如果其中某些引用计数的数值减为0,代表没有进程使用该资源,可以释放;

把存放在task_struct的exit_code成员中的任务退出代码置为exit()提供的代码中,或完成由其它内核机制规定的退出动作,退出代码存放在这里供父进程随时检索;

调用exit_notify()向父进程发送信号,将子进程的父进程重新设置为线程组中的其它线程或init进程,并把进程状态设置为TASK_ZOMBIE;

最后do_exit()调用schedule()切换到其它进程,因为处于TASK_ZOMBIE状态的进程不会再被调度,所以这是进程执行的最后一段代码。

do_exit()实现如下:kernelexit.c

NORET_TYPE void do_exit(long code)
{
struct task_struct *tsk = current;

if (in_interrupt())
panic("Aiee, killing interrupt handler!");
if (!tsk->pid)
panic("Attempted to kill the idle task!");
if (tsk->pid == 1)
panic("Attempted to kill init!");
tsk->flags |= PF_EXITING;
del_timer_sync(&tsk->real_timer);

fake_volatile:
#ifdef CONFIG_BSD_PROCESS_ACCT
acct_process(code);
#endif
__exit_mm(tsk);

lock_kernel();
sem_exit();
__exit_files(tsk);
__exit_fs(tsk);
exit_sighand(tsk);
exit_thread();

if (current->leader)
disassociate_ctty(1);

put_exec_domain(tsk->exec_domain);
if (tsk->binfmt && tsk->binfmt->module)
__MOD_DEC_USE_COUNT(tsk->binfmt->module);

tsk->exit_code = code;
exit_notify();
schedule();
BUG();
/*
* In order to get rid of the "volatile function does return" message
* I did this little loop that confuses gcc to think do_exit really
* is volatile. In fact it's schedule() that is volatile in some
* circumstances: when current->state = ZOMBIE, schedule() never
* returns.
*
* In fact the natural way to do all this is to have the label and the
* goto right after each other, but I put the fake_volatile label at
* the start of the function just in case something /really/ bad
* happens, and the schedule returns. This way we can try again. I'm
* not paranoid: it's just that everybody is out to get me.
*/
goto fake_volatile;
}

do_exit()执行完毕后,与进程相关的资源被析构,但它还会占用内核栈、thread_info和task_struct结构,以便于向父进程提供信息。一旦父进程检索到信息并通知内核这些信息可释放,那么由进程所持有的剩余内存也将被全部释放。

上面说到,do_exit()执行完毕后,进程处于TASK_ZOMBIE状态且无法执行,但是系统还保留了获取到它的信息。所以,进程终结时,进程资源的释放和进程描述符的删除是分开进行的。只有当父进程获得子进程终结的消息并告知内核可释放时,子进程的task_struct结构才会被释放。

父进程通过调用wait()函数族来等待子进程的终结,wait()函数族都是通过wait4()系统调用来实现的,它的用途是挂起调用该函数的进程,直到有一个子进程退出,并返回退出子进程的PID,另外,调用该函数时提供的指针会包含子进程退出时的退出代码。

等到可以删除进程描述符时,会调用release_task()函数,它的主要工作如下:

调用free_uid()来减少该进程拥有者的进程使用计数。Linux用一个单用户高速缓存统计和记录每个用户占用的进程数目、文件数目。如果这些数目都变为0,表示这个用户没有使用任何进程和文件,那么这块缓存可以释放。

调用unhash_process()从pidhash上删除该进程,同时从task_list中删除该进程。

如果这个进程正在被ptrace跟踪,那么将跟踪进程的父进程重设为其最初的父进程并将它从ptract_list上删除。

调用put_task_struct()释放进程内核栈和thread_info结构所占的页,释放task_struct所占用的slab高速缓存。

这样进程所独享的资源和进程描述符就全部释放掉了。

release_task()实现如下:kernelexit.c

static void release_task(struct task_struct * p)
{
if (p != current) {
#ifdef CONFIG_SMP
/*
 * Wait to make sure the process isn't on the
 * runqueue (active on some other CPU still)
 */
for (;;) {
task_lock(p);
if (!p->has_cpu)
break;
task_unlock(p);
do {
barrier();
} while (p->has_cpu);
}
task_unlock(p);
#endif
atomic_dec(&p->user->processes);
free_uid(p->user);
unhash_process(p);

release_thread(p);
current->cmin_flt += p->min_flt + p->cmin_flt;
current->cmaj_flt += p->maj_flt + p->cmaj_flt;
current->cnswap += p->nswap + p->cnswap;
/*
 * Potentially available timeslices are retrieved
 * here - this way the parent does not get penalized
 * for creating too many processes.
 *
 * (this cannot be used to artificially 'generate'
 * timeslices, because any timeslice recovered here
 * was given away by the parent in the first place.)
 */
current->counter += p->counter;
if (current->counter >= MAX_COUNTER)
current->counter = MAX_COUNTER;
free_task_struct(p);
} else {
printk("task releasing itselfn");
}
}

上面讨论的都是子进程退出时父进程如何释放其资源。但是,如果父进程先退出呢?当父进程先于子进程退出时,子进程将变为孤儿进程,孤儿进程在退出时将会永远处于僵死状态,无法释放内存,这就造成了资源的浪费。所以必须有一种机制来保证父进程先于子进程退出时,子进程可以得到一个新的父进程。一般来说,会将子进程在当前线程组中找一个线程作为父进程,或者让init来做它们的父进程。

在do_exit()中会调用notify_parent(),而notify_parent()通过调用forget_original_parent()来寻找父进程,其实现如下kernelexit.c

/*
* When we die, we re-parent all our children.
* Try to give them to another thread in our process
* group, and if no such member exists, give it to
* the global child reaper process (ie "init")
*/
static inline void forget_original_parent(struct task_struct * father)
{
struct task_struct * p, *reaper;

read_lock(&tasklist_lock);

/* Next in our thread group */
reaper = next_thread(father);
if (reaper == father)
reaper = child_reaper;

for_each_task(p) {
if (p->p_opptr == father) {
/* We dont want people slaying init */
p->exit_signal = SIGCHLD;
p->self_exec_id++;
p->p_opptr = reaper;
if (p->pdeath_signal) send_sig(p->pdeath_signal, p, 0);
}
}
read_unlock(&tasklist_lock);
}

首先将reaper设置为该进程所在线程组内的其它进程。如果进程组内没有其它进程就将reaper设置为child_reaper,也就是init进程,然后遍历所有子进程并为它们设置新的父进程。遍历子进程链表给每个子进程设置新的父进程。在2.6内核(代码展示为2.4内核),还要遍历ptrace子进程链表,当一个进程被跟踪时,他被暂时设定为调试进程的子进程,此时如果它的父进程退出了,系统会为它和它的所有兄弟重新找一个父进程。

当系统成功为进程找到并设置了新的父进程,就不会出现驻留僵尸进程了。init进程会调用wait()来等待其它子进程,清除所有与其相关的僵死进程。

相关推荐

登录即可解锁
  • 海量技术文章
  • 设计资源下载
  • 产业链客户资源
  • 写文章/发需求
立即登录

Linux、C、C++、Python、Matlab,机器人运动控制、多机器人协作,智能优化算法,贝叶斯滤波与卡尔曼滤波估计、多传感器信息融合,机器学习,人工智能。