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

1.进程

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

内核需要有效而透明的管理所有细节。

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

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

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

2.进程描述符

内核把进程的列表存放在一个叫做任务队列的双向环形链表中,链表中每一项(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()来检查其子进程,清除所有与其相关的僵死进程。

posted @ 2016-04-06 19:33  20135132  阅读(183)  评论(0编辑  收藏  举报