《linux内核设计与实现》第三章

1.进程css

进程就是正在执行的程序代码的实时结果,不只包含可执行代码,还包括其余资源,好比:打开的文件,挂起的信号,内核内部数据结构,处理器状态,一个或多个具备内存映射的内存地址空间及一个或多个执行线程,全局变量数据段等。算法

内核须要有效而透明的管理全部细节。缓存

线程,每一个线程拥有一个独立的程序计数器,进程栈和一组寄存器。内核调度对象是线程而不是进程。数据结构

现代操做系统提供两种虚拟机制:虚拟处理器和虚拟内存,线程之间能够共享虚拟内存,但每一个都有各自的虚拟处理器。dom

Linux中,新进程是由fork()来实现的,fork()实际上由clone()系统调用实现,程序经过exit()系统调用退出执行,这个函数会终结进程并释放其占用的资源,父进程能够经过wait4()查询子进程是否终结。进程退出执行后被设置为僵死状态,直到他父进程调用wait()或waitpid()。函数

2.进程描述符spa

内核把进程的列表存放在一个叫作任务队列的双向环形链表中,链表中每一项(task_struct类型)都称为进程描述符。操作系统

进程描述符包括一个进程的具体全部信息:打开的文件,进程地址空间,挂起的信号,进程状态等。在中定义。线程

Linux经过slab分配器分配task_struct结构,这样能达到对象复用和缓存目的。指针

Linux在栈底或栈顶建立一个新的结构struct thread_info来存放task_struct

 

  1. struct thread_info {
  2.     struct task_struct *task;
  3.     struct exec_domain *exec_domain;
  4.     __u32                 flags;
  5.     __u32                 status;
  6.     __u32                 cpu;
  7.     int preempt_count;
  8.     mm_segment_t          addr_limit;
  9.     struct restart_block  restart_block;
  10.     void *sysenter_return;
  11.     int uaccess_err;
  12. };

 

3.进程状态

task_struct中的state域描述了进程的当前状态,每一个进程必处于如下5个状态之一。

TASK_RUNNING—进程是可执行的,正在执行或者在运行队列中等待执行

TASK_INTERRUPTIBLE—进程正在睡眠(阻塞),等待某个条件达成。该条件一旦到来就进入TASK_RUNNING状态,能够接收信号而提早唤醒。

TASK_UNINTERRUPTIBLE—除了不能响应信号,与TASK_INTERRUPTIBLE同样,这个状态,进程必须在等待时不受干扰或等待事件很快就会发生时出现。

__TASK_TRACED—被其余进程跟踪的进程,好比经过ptrace对调试程序进行跟踪

__TASK_STOPPED—中止。进程没有投入运行,也不能投入运行。这种状况通常发生在进程收到SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU等信号的时候,此外在调试期间接收到任何信号,都会使进程进入这种状态

设置进程,set_task_state(task,state),必要的时候,它会设置内存屏蔽来强制其余处理器做从新排序(SMP系统中才有必要)

进程上下文:一个程序调用了系统调用,或触发了某个异常,它就陷入了内核空间。此时,内核“表明进程执行”,并处于进程上下文中,这里current宏是有效的;这个过程进程是能够被调度的。

中断上下文:系统不表明进程执行,而是执行一个中断处理函数;不能被调度。

4.进程家族树

全部进程都是init进程的后代,内核在系统启动的最后阶段启动init进程,该进程读取系统初始化脚本并执行其余的相关程序。

每一个进程都有本身的父进程,和零个或多个子进程,全部拥有同一个父进程的进程是兄弟进程。

 

  1. //访问父进程
  2. struct task_struct *my_parent = current->parent;
  3. //依次访问全部子进程
  4. struct task_struct *task;
  5. struct list_head *list;
  6. list_for_each(list, &current->children) {
  7.     task = list_entry(list, struct task_struct, sibling);
  8.     /* task now points to one of current\'s children */
  9. }
  10. //遍历系统中全部进程
  11. list_entry(task->tasks.next, struct task_struct, tasks)
  12. list_entry(task->tasks.prev, struct task_struct, tasks)

5.进程建立

(1)许多操做系统都提供了产生进程的机制,首先在新的地址空间建立进程,读入可执行文件,最后开始执行。Unix吧这个步骤分解到两个单独的函数去执行,fork()和exec()。首先fork()经过拷贝当前进程建立一个子进程,其与父进程区别是PID,PPID,某些资源和统计量(如挂起信号,不用继承),exec负责读取可执行文件并将其载入地址空间开始运行。

(2)写时拷贝

是一种能够推迟甚至免除拷贝数据的技术,内核此时并不复制,而是与父进程共享一个拷贝。只有在须要写入时,才会复制数据。

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

(3)fork建立进程过程

fork(),vfork()和__clone()库函数都根据各自须要的参数标识去调用clone()->调用do_fork()->调用copy_process(),copy_process()完成以下过程

①调用dup_task_struct为新进程建立一个新的内核栈,thread_info结构和task_struct,这些值与当前进程的值相同,进程描述符也相同。

②检查确保建立子进程后,当前用户拥有的进程数没有超出为其分配的资源限制

③进程描述符内的许多成员都被清零或初始化,以与父进程区分开来,统计信息通常不继承,task_struct中的大多数数据依然未修改。

④子进程状态被设置为TASK_UNINTERRUPTIBLE,以保证它不会投入运行。

⑤copy_process调用copy_flags(),更新task_struct的flag成员。

⑥调用alloc_pid()为新进程分配一个有效的PID。

⑦根据传递给clone()的参数标识,拷贝或共享打开的文件,信号处理函数,进程地址空间等。

⑧最后copy_process作收尾工做,返回一个指向子进程的指针。

返回到do_fork(),若是copy_process()成功返回,子进程被唤醒并投入运行,内核有意选择子进程首先执行。(父进程先执行可能会向地址空间写入)

(4)vfork()

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

6.线程在Linux中的实现

(1)从内核角度来看,并无线程这个概念,Linux把全部线程都看成进程来实现,内核并无准备特别的调度算法或是定义特别的数据结构来表征线程,它仅仅被视为一个与其余进程共享某些资源的进程。每一个线程都拥有惟一隶属于本身的task_struct.

对于多个线程并行,Linux只是为每一个线程建立普通的task_struct的时候指定他们共享某些资源。

(2)建立线程

线程建立与普通进程建立相似,只不过在调用clone()的时候须要会传递一些参数标识来指明须要共享的资源

clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);

普通fork()

clone(SIGCHLD, 0);

vfork()

clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0);

传递给clone()的参数标志据诶的那个了新建立进程的行为和父子进程之间共享资源种类。

(3)内核线程

内核常常须要在后台执行一些操做,这种任务能够经过内核线程来完成---独立运行在内核空间的标准线程。它和普通线程的区别在于,内核线程没有独立的进程空间(指向地址空间的mm指针为NULL),只在内核运行。跟普通线程同样能够被调度,也能够被抢占。

内核线程只能由其余内核线程建立,Linux是经过从kthread内核进程衍生出全部新的内核线程的。内核建立新内核线程方法:

 

 

  1. struct task_struct *kthread_create(int (*threadfn)(void *data),
  2.                    void *data,
  3.                  const char namefmt[],
  4.                  ...)

 

新的任务是有kthread进程调用clone()建立的。新进程将运行threafn函数,给其传递参数data,namefmt接受可变参数列表。

新建立的进程处于不可运行状态,须要经过wake_up_process()明确的唤醒它,它不会主动运行。

建立一个进程并让它运行起来,能够调用

 

  1. #define kthread_run(threadfn, data, namefmt, ...) \\
  2. ({ \\
  3.        struct task_struct *__k \\
  4.               = kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \\
  5.        if (!IS_ERR(__k)) \\
  6.               wake_up_process(__k); \\
  7.        __k; \\
  8. })

实际上就是简单的调用了kthread_create()和wake_up_process()。

内核线程启动后就一直运行直到调用do_exit(),或者内核其余部分调用kthread_stop()退出。传递给kthread_stop()的参数是kthread_create()函数返回的task_struct结构的地址。

int kthread_stop(struct task_struct *k)

7.进程终结

(1)一个进程终结时,内核必须释放它占有的资源并把这告知其父进程。

显示调用exit()(C编译器会在main()函数的返回点后面放置调用exit()的代码),或者当进程接收到它既不能处理也不能忽略的信号或异常时,它还可能被动的终结。

无论以何种方式终结,大部分都要靠do_exit()来完成,它作如下工做:

①将task_struct中的标志成员设置为PF_EXITING。

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

③若是BSD的进程记帐功能开启,do_exit()会调用act_update_integrals()来输出记帐信息。

④调用exit_mm()函数释放进程占用的mm_struct,若是没有别的进程使用它们(即该地址空间没有被共享),就完全释放他们。

⑤调用sem__exit(),若是进程排队等候IPC信号,它则离开队列。

⑥调用exit_files()和exit_fs(),分别递减文件描述符,文件系统数据的引用计数。若是某个引用计数为0,就表明没有进程在使用相应的资源,此时能够释放。

⑦把存放在task_struct的exit_code成员的任务推出代码置为由exit()提供的推出代码,或者去完成其余由内核机制规定的推出动做,退出代码存放在这里供父进程随时检索。

⑧exit_notify()向父进程发信号,给子进程从新找养父,养父为线程组中的其余线程或者为init进程,并把进程状态(task_struct的exit_state中)置为EXIT_ZOBIE.

⑨do_exit()调用schedule()切换到新的进程。处于EXIT_ZOBIE的进程永远不会再被调度,do_exit()永不返回。

至此,进程相关的全部资源都被释放(假设是独享),如今占用的资源就只有内核栈,thread_info结构和task_struct结构,此时进程存在的惟一目的是向它的父进程提供信息。

(2)删除进程描述符

调用do_exit()以后,线程已经僵死,但系统还保留有其进程描述符,这样系统有办法在子进程和终结后仍能得到它的信息。进程终结时所需的清理工做和删除进程描述符分开执行。

wait()函数族都是调用wait4()来实现的,它的标准动做是挂起调用它的进程,直到其中的一个子进程推出,此时函数会返回孩子进程的PID,且调用该函数时提供的指针会包含子函数退出时的代码。

当最终须要释放进程描述符是,会调用release_task()。

①调用__exit_signal()à调用_unhash_process()à调用detach_pid()从pidhash上删除该进程,同时也要从任务队列中删除该进程。

②_exit_signal()释放目前僵尸进程所使用的剩余资源,并进行最终统计和记录。

③若是这个进程是线程组最后一个进程,而且领头进程已经死掉,那么release_task()就要通知僵死的领头进程的父进程。

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

至此,进程描述符和全部进程独享的资源就所有释放掉了。

(3)孤儿进程

若是父进程在子进程以前退出,就必须为子进程找到新父亲,以避免进程永远处于僵死状态,耗费内存。解决方法是,给子进程在当前进程组找一个线程做为父亲,若是不行,就让init做为其父进程。

do_exit()会调用exit_notify(),该函数会调用forget_original_parent(),然后会调用find_new_reaper()来执行寻父过程。

 

  1. static struct task_struct *find_new_reaper(struct task_struct *father)
  2. {
  3.     struct pid_namespace *pid_ns = task_active_pid_ns(father);
  4.     struct task_struct *thread;
  5.     
  6.     thread = father;
  7.     while_each_thread(father, thread) {
  8.         if (thread->flags & PF_EXITING)
  9.             continue;
  10.         if (unlikely(pid_ns->child_reaper == father))
  11.             pid_ns->child_reaper = thread;
  12.         return thread;
  13.     }
  14.     if (unlikely(pid_ns->child_reaper == father)) {
  15.         write_unlock_irq(&tasklist_lock);
  16.         if (unlikely(pid_ns == &init_pid_ns))
  17.             panic(\"Attempted to kill init!\");
  18.             zap_pid_ns_processes(pid_ns);
  19.             write_lock_irq(&tasklist_lock);
  20.             /*
  21.             * We can not clear ->child_reaper or leave it alone.
  22.             * There may by stealth EXIT_DEAD tasks on ->children,
  23.             * forget_original_parent() must move them somewhere.
  24.             */
  25.             pid_ns->child_reaper = init_pid_ns.child_reaper;
  26.         }
  27.     return pid_ns->child_reaper;
  28.     }
  29. //找到合适父进程后,只要遍历全部子进程并为他们设置新的父进程
  30. reaper = find_new_reaper(father);
  31. list_for_each_entry_safe(p, n, &father->children, sibling) {
  32.     p->real_parent = reaper;
  33.     if (p->parent == father) {
  34.     BUG_ON(p->ptrace);
  35.     p->parent = p->real_parent;
  36.     }
  37.     reparent_thread(p, father);
  38. }
  39. //而后调用ptrace_exit_finish()一样进行寻父过程,不过是给ptraced的子进程寻父
  40. void exit_ptrace(struct task_struct *tracer)
  41. {
  42.     struct task_struct *p, *n;
  43.     LIST_HEAD(ptrace_dead);
  44.     write_lock_irq(&tasklist_lock);
  45.     list_for_each_entry_safe(p, n, &tracer->ptraced, ptrace_entry) {
  46.         if (__ptrace_detach(tracer, p))
  47.             list_add(&p->ptrace_entry, &ptrace_dead);
  48.      }
  49.     
  50.     write_unlock_irq(&tasklist_lock);
  51.     BUG_ON(!list_empty(&tracer->ptraced));
  52.     list_for_each_entry_safe(p, n, &ptrace_dead, ptrace_entry) {
  53.         list_del_init(&p->ptrace_entry);
  54.         release_task(p);
  55.     }
  56. }

这段代码遍历两个链表:子进程链表和ptrace子进程链表。

在一个单独的被ptrace跟踪的子进程链表中搜索相关的兄弟进程---用两个相对较小的链表减轻了遍历全部系统进程的消耗。

一旦系统为进程成功找到和设置了新父进程,就不会再出现驻留僵死进程的危险,init进程会例行调用wait()来检查其子进程,清除全部与其相关的僵死进程。

相关文章
相关标签/搜索