Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1028151
  • 博文数量: 26
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 437
  • 用 户 组: 普通用户
  • 注册时间: 2019-09-08 12:19
个人简介

关于个人介绍,既然你诚心诚意的看了 我就大发慈悲的告诉你 为了防止世界被破坏 为了维护世界的和平 贯彻爱与真实的邪恶 可爱又迷人的反派角色 我们是穿梭在银河的火箭队 白洞,白色的明天在等着我们

文章分类

分类: LINUX

2019-10-12 18:32:45

    在<Linux应用程序 启动流程>中,我们引出了exit系统调用函数,我们依然以hello world为例子程序
点击(此处)折叠或打开
  1. #include <stdio.h>

  2. int main (int argc, char *argv[])
  3. {
  4.     printf ("Hello World\n");

  5.     return 0;
  6. }

保存为hello.c,我们可以通过gcc hello.c -o hello得到可执行程序hello.
    由于我们认为main函数返回后,代表程序结束,实际上,当main函数返回的时候,后续还调用了一个exit函数(C库中实现)。
点击(此处)折叠或打开

  1. xxxxx
  2. {
  3.     .......
  4.     result = main(argc, argv);
  5.     exit(result);
  6. }
    如上图,exit函数是一个系统调用,它主要是由Linux kernel实现用于回收进程的资源而产生的。exit函数和execve函数一样叫做 一去不复返。对于exit函数来说,并没有返回的概念,它的流程最后就是直接切换进程task_struct。下面是exit函数的实现:
点击(此处)折叠或打开
  1. SYSCALL_DEFINE1(exit, int, error_code)
  2. {
  3.     do_exit((error_code&0xff)<<8);
  4. }
  5. // 源码在kernel/exit.c文件当中
    如《Linux内核之execve函数》描述的一样,其中SYSCALL_DEFINE1是一个宏,表示这是一个含有1个参数的系统调用这个宏会产生一个sys_exit函数。也就是当应用程序调用exit函数的时候,CPU产生系统调用中断,中断处理函数通过查表(sys_call_table),通过对应的系统调用号找到sys_exit函数并开始执行(R7/W8存放的系统调用号,参数以此类推),sys_exit函数到do_exit并没有做什么操作只是简单调用,因此现在直接到do_exit函数,调用栈如下:
点击(此处)折叠或打开
  1.   exit                  (user space)
  2. ---|----------------------------------------------------------------------------
  3.    v                    (kernel space)
  4. el0_sync     (arm64同步异常中断处理函数)
  5.     el0_svc (查找sys_call_table,获取到sys_execve函数地址,并运行
  6.         sys_exit
  7.             do_exit
    这里以hello可执行程序为例,并把它作为一个死者的身份来解说这个函数,do_exit函数实现如下:
点击(此处)折叠或打开
  1. #define __noreturn  __attribute__((noreturn))

  2. void __noreturn do_exit(long code)
  3. {
  4.     ......
  5.     exit_signals(tsk); // 设置PF_EXITING标识,告诉其他访问task_struct,当前task已经死亡
  6.     
  7.     // 现在流行薄葬啊,什么资源都要搜刮干净,赤裸而来赤裸而去
  8.     tsk->exit_code = code; // 设置退出状态码,来自于exit()

  9.     // 获取当前进程所在的线程组里是否还有成员活着。其中signal是signal_struct结构,Linux中,同一线程组里的所有线程共享信号资源。
  10.     group_dead = atomic_dec_and_test(&tsk->signal->live);
  11.     ..........
  12.     exit_mm(tsk);  // 会尝试去释放mm_struct结构
  13.     exit_sem(tsk); // 释放信号量资源
  14.     exit_shm(tsk);  //释放共享内存资源
  15.     exit_files(tsk); // 释放当前进程打开的文件资源put_files_struct(tsk->files)

  16.     // 如果fs->users计数器为0,释放工作目录(root/pwd)对应资源free_fs_struct(tsk->fs)
  17.     exit_fs(tsk);   
  18.     if (group_dead)
  19.         disassociate_ctty(1);
  20.     exit_task_namespaces(tsk); // 释放进程命名空间资源
  21.     exit_task_work(tsk);       // 依次执行由task_work_add增加到task->task_works的函数回调
  22.     exit_thread(tsk);          // CONFIG_HAVE_EXIT_THREAD配置是否需要执行
  23.     sched_autogroup_exit_task(tsk);
  24.     exit_notify(tsk, group_dead);  // 收完死者值钱的东西后,通知亲属,处理后事
  25.     do_task_dead();      // 设置当前状态为D状态, 切换进程,一去不复返
  26.     // 这里永远不会允许
  27. }

  28. void __noreturn do_task_dead(void)
  29. {
  30.     __set_current_state(TASK_DEAD);
  31.     __schedule(false); //主动让出CPU,发生系统调度,等待完成切换后, 释放task_struct结构。
  32.     for (;;)
  33.          cpu_relax();
  34. }
    以上函数do_exit省略了部分非关键函数,如有效性检查、统计、审计、block_plug、perf_event和cgroup等。
    下面详细描述一下关键函数exit_notify怎么处理后事的,在描述之前,先普及3个概念:
  1. 孤儿进程组:该组中每个成员的父进程要么是该组的一个成员,要么不是该组所属会话的成员。或者用另外一种表示一个进程组不是孤儿进程组的条件是:该组中有一个进程,其父进程在属于同一个会话的另一个组中
  2. 孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程
  3. 进程组:进程组,每个进程组有一个领头进程。进程组是一个或多个进程的集合,通常它们与一组作业相关联,可以接受来自同一终端的各种信号。
    exit_notify函数作用就是为当前退出的进程的子进程找到合适的父进程(进程退出,他的子进程必然成孤儿进程,需要新父进程),通常这个新的父进程是init进程(默认系统启动后只有一个pid命名空间叫init_pid_ns,而这个pid命名空间的1号进程就是init进程,多个pid命名空间不再当前考虑范围内),其实现如下:
点击(此处)折叠或打开
  1. static void exit_notify(struct task_struct *tsk, int group_dead)
  2. {
  3.     ......
  4.     //因该tsk死亡了,这里需要为tsk的子进程选取新的父进程:学名叫刘备托孤,当然没有儿子
  5.     //也就不用托孤了,这函数也会直接返回
  6.     forget_original_parent(tsk, &dead);

  7.     // 世事无常,这一脉的线程组里已经空了,那么如果这个死者所在的进程组是孤儿进程组,则向这个孤儿组内还有stop job的线程发送SIGHUP和SIGCONT信号吧
  8.     if (group_dead)
  9.         kill_orphaned_pgrp(tsk->group_leader, NULL);

  10.     if (unlikely(tsk->ptrace)) { //当前进程被trace
  11.         int sig = thread_group_leader(tsk) &thread_group_empty(tsk) &&
  12.                     !ptrace_reparented(tsk) ?
  13.             tsk->exit_signal : SIGCHLD;
  14.         autoreap = do_notify_parent(tsk, sig);
  15.     } else if (thread_group_leader(tsk))
  16.    //当前线程为线程组组长,如果线程组不为空,就不要通知组长的父进程它已经死了的消息,即autoreap=false
  17.     // 即这个线程组即使为僵尸,也不回收它的资源
  18.         autoreap = thread_group_empty(tsk) &&
  19.     // 如果线程组为空,就通知它的父进程,死亡原因(主要是回收资源相关
  20.             do_notify_parent(tsk, tsk->exit_signal);
  21.     } else {
  22.     // 当前线程不是线程组组长,直接设置autoreap为true,宣布dead,回收资源
  23.         autoreap = true;
  24.     }
  25.     // 这里如果do_notify_parent返回是autoreap =true即EXIT_DEAD,说明父进程不需要子进程的退出码,即父进程不需要调用wait/waitpid等函数;否则就设置成EXIT_ZOMBIE,等父进程调用了wait函数后,再释放这个进程的task_struct
  26.     tsk->exit_state = autoreap ? EXIT_DEAD : EXIT_ZOMBIE;
  27.     // 如果进程已死,父进程不需要waitpid, 就放到资源释放链表dead里面去,等待释放
  28.     if (tsk->exit_state == EXIT_DEAD)
  29.         list_add(&tsk->ptrace_entry, &dead);

  30.     // 引用计数小于0,唤醒线程组退出task去执行
  31.     if (unlikely(tsk->signal->notify_count < 0))
  32.         wake_up_process(tsk->signal->group_exit_task);
  33.     write_unlock_irq(&tasklist_lock);

  34.     // 遍历资源释放链表dead,为每一个在表里的释放资源,如果释放到当前线程,并且线程已空,而
  35.     // 且线程组组长也是僵尸,那么久将线程组组长一起释放了
  36.     list_for_each_entry_safe(p, n, &dead, ptrace_entry) {
  37.         list_del_init(&p->ptrace_entry);
  38.         // 尝试释放资源
  39.         release_task(p);
  40.     }
  41. }
    从上面可以看出,死者拜托函数forget_original_parent进行托孤(pid空间的1号进程不是指pid=1,而是指它是这个命名空间的第一个进程。通常情况只有一个命名空间,而这个命名空间的1号进程为init进程),实现如下:

点击(此处)折叠或打开

  1. static void forget_original_parent(struct task_struct *father,
  2.                     struct list_head *dead)
  3. {
  4.     struct task_struct *p, *t, *reaper;

  5.     //当前进程如果为ptrace,则退出所有ptrace下面的进程
  6.     if (unlikely(!list_empty(&father->ptraced))) { 
  7.         exit_ptrace(father, dead);//退出所有被trace的task,并将已死的进程加入到dead链表
  8.     }

  9.     /* 选择合适的默认收养者,继父,通常为当前pid空间的1号进程,我们叫它村长 */
  10.     reaper = find_child_reaper(father);
  11.     if (list_empty(&father->children)) // 当前进程没有子进程,就直接返回了,不需要托孤了
  12.         return;

  13.     // 作为该pid空间的默认收养者,虽然有承担收养遗孤的责任,但是也不能什么孤儿都往这里放啊,还是要先看看孤儿的亲戚有没有愿意收养的吧
  14.     // 尝试从亲戚中挑选一个收养者,pid命名空间的1号进程(村长)作为缺省的收养者,
  15.     reaper = find_new_reaper(father, reaper);
  16.     // 将当前进程下的所有娃儿的父亲都改成reaper(这个reaper可能是村长,也可能是他的祖先)。
  17.     list_for_each_entry(p, &father->children, sibling) {
  18.         for_each_thread(p, t) {
  19.             t->real_parent = reaper; // 设置新的父进程real_parent ,在有调试的时候,parent 指向ptrace进程
  20.             BUG_ON((!t->ptrace) != (t->parent == father));
  21.             if (likely(!t->ptrace))
  22.                 t->parent = t->real_parent;
  23.             if (t->pdeath_signal)
  24.                 group_send_sig_info(t->pdeath_signal,
  25.                          SEND_SIG_NOINFO, t);
  26.         }
  27.         // 如果死者和继父不是同一个线程组(不是一家)
  28.         // (继父这里很可能就是init进程或者Pid空间的1号进程),那没办法,把死者的所有儿子状态为
  29.         // EXIT_ZOMBIE的告诉它的继父;同时如果变成了孤儿组,则向孤儿组内所有还有stop jobs的进程
  30.         // 发送SIGHUP和SIGCONT信号。
  31.         if (!same_thread_group(reaper, father))
  32.             reparent_leader(father, p, dead);
  33.     }
  34.     // 将当前进程的子进程 全部加到继父进程的链表上,从此这个继父就是这些子进程的父亲
  35.     list_splice_tail_init(&father->children, &reaper->children);
  36. }
    find_child_reaper函数用于查找对应PID空间的1号进程,这里的1号并不是指它的PID=1,而是这个进程是当前pid命名空间的祖先进程类似于init进程,对应函数实现如下:

点击(此处)折叠或打开

  1. static struct task_struct *find_child_reaper(struct task_struct *father)
  2.     __releases(&tasklist_lock)
  3.     __acquires(&tasklist_lock)
  4. {
  5.     struct pid_namespace *pid_ns = task_active_pid_ns(father);
  6.     struct task_struct *reaper = pid_ns->child_reaper;

  7.     // 查看死者是否为所在的pid空间的1号进程(村长) 如果不是,就返回pid空间的1号进程,作为缺省收养者
  8.     if (likely(reaper != father)
  9.         return reaper;

  10.     // pid空间的1号进程就是死者,则需要从死者所在的线程组 选择一个亲戚作为备用收养者(下一个活动线程
  11.     // 哎,程序也讲究 世袭制,村长从亲戚中选
  12.     reaper = find_alive_thread(father);
  13.     if (reaper) {
  14.         // 设置这个线程为当前pid空间的1号进程(村长)
  15.         pid_ns->child_reaper = reaper;
  16.         return reaper;
  17.     }

  18.     write_unlock_irq(&tasklist_lock);
  19.     // 如果当前进程没有线程,并且自己又是1号进程,而且还是init_pid_ns,说明退出的是init进程,抛出异常
  20.     // 创世神已死,留着世界干啥,panic掉
  21.     if (unlikely(pid_ns == &init_pid_ns)) {
  22.         panic("Attempted to kill init! exitcode=0x%08x\n",
  23.             father->signal->group_exit_code ?: father->exit_code);
  24.     }
  25.     zap_pid_ns_processes(pid_ns);
  26.     write_lock_irq(&tasklist_lock);
  27.     // 死者继续上,没办法,不安宁
  28.     return father;
  29. }
      由于村长不同意总是收养孤儿,因此,拜托find_new_reaper函数去找一个更合适的收养者,如果实在没有找到,再找他吧,实现如下:

点击(此处)折叠或打开

  1. static struct task_struct *find_new_reaper(struct task_struct *father,
  2.                      struct task_struct *child_reaper)
  3. {
  4.     struct task_struct *thread, *reaper;

  5.     // child_reaper为find_child_reaper获取到的推荐收养者,但是收养者不同意,要求先从它的兄弟姐妹选择
  6.    // 从当前进程的线程组(兄弟姐妹)选择一个alive的线程,如果线程存在,就用这个线程作为收养者
  7.     thread = find_alive_thread(father);
  8.     if (thread)
  9.         return thread;

  10.     // 这货没有兄弟姐妹,就只有找找他是否有遗属,说明他祖宗愿不愿意作为收养者,如果有,就往上去找他的祖宗来收养, has_child_subreaper为真,表示死者有交代他们家有祖先愿意做收养者
  11.     if (father->signal->has_child_subreaper) {
  12.         //从死者往上找,直到child_reaper为止,如果找到备胎愿意收养,就用这个祖宗线程组下
  13.         //的线程作为收养者。如果找到init了,也没找到,那就只要让pid空间的1号来收养了,能者多劳嘛。
  14.         for (reaper = father;
  15.          !same_thread_group(reaper, child_reaper);
  16.          reaper = reaper->real_parent) {
  17.             /* 都找到创世神这里了,都没人愿意收养,只有跳出去找对应pid空间的1号进程了*/
  18.             if (reaper == &init_task)
  19.                 break;
  20.             // 这个祖先表明,他就是那个收养者,如果不是就continue
  21.             if (!reaper->signal->is_child_subreaper)
  22.                 continue;
  23.             // 找到一个合适的祖先,从这个辈分的祖先里面选一个或者的老者来收养
  24.             thread = find_alive_thread(reaper);
  25.             if (thread)
  26.                 return thread;
  27.         }
  28.     }
  29.     // 村长:我曹,果然还是我,不让人省心啊
  30.     return child_reaper;
  31. }
    reparent_leader函数如下:

点击(此处)折叠或打开

  1. static void reparent_leader(struct task_struct *father, struct task_struct *p,
  2.                 struct list_head *dead)
  3. {
  4.     // 进程已经死了,就直接返回了
  5.     if (unlikely(p->exit_state == EXIT_DEAD))
  6.         return;

  7.     /* We don't want people slaying init. */
  8.     p->exit_signal = SIGCHLD;

  9.     // 查看进程是否为僵尸,并且所在线程组为空,就向它的新父亲,发送SIGCHLD信号
  10.     if (!p->ptrace &&
  11.      p->exit_state == EXIT_ZOMBIE && thread_group_empty(p)) {
  12.         if (do_notify_parent(p, p->exit_signal)) {
  13.             p->exit_state = EXIT_DEAD;
  14.             list_add(&p->ptrace_entry, dead);
  15.         }
  16.     }
  17.    // 如果是孤儿进组,则向进程组内所有还有stop jobs的进程发送SIGHUP和SIGCONT信号
  18.    kill_orphaned_pgrp(p, father);
  19. }
    UNIX中 在父进程终止后,进程组成为孤儿进程组,POSIX要求向新的孤儿进程组中处于停止状态的每一个进程发送挂断信号(SIGHUP),接着又向其发送继续信号(SIGCONT)。
    kill_orphaned_pgrp函数实现如下:

点击(此处)折叠或打开

  1. static void
  2. kill_orphaned_pgrp(struct task_struct *tsk, struct task_struct *parent)
  3. {
  4.     struct pid *pgrp = task_pgrp(tsk);
  5.     struct task_struct *ignored_task = tsk;

  6.     if (!parent)
  7.         /* exit: our father is in a different pgrp than
  8.          * we are and we were the only connection outside.
  9.          */
  10.         parent = tsk->real_parent;
  11.     else
  12.         /* reparent: our child is in a different pgrp than
  13.          * we are, and it was the only connection outside.
  14.          */
  15.         ignored_task = NULL;
  16.     // UNIX中 在父进程终止后,进程组成为孤儿进程组,POSIX要求向新的孤儿进程组中处于停止状态的
  17.   // 每一个进程发送挂断信号(SIGHUP),接着又向其发送继续信号(SIGCONT)。
  18.     if (task_pgrp(parent) != pgrp &&
  19.      task_session(parent) == task_session(tsk) &&
  20.      will_become_orphaned_pgrp(pgrp, ignored_task) &&
  21.      has_stopped_jobs(pgrp)) {
  22.         __kill_pgrp_info(SIGHUP, SEND_SIG_PRIV, pgrp);
  23.         __kill_pgrp_info(SIGCONT, SEND_SIG_PRIV, pgrp);
  24.     }
  25. }
    release_task释放task_struct资源的函数,实现如下:
点击(此处)折叠或打开
  1. void release_task(struct task_struct *p)
  2. {
  3.     struct task_struct *leader;
  4.     int zap_leader;
  5. repeat:
  6.     rcu_read_lock();
  7.     atomic_dec(&__task_cred(p)->user->processes);
  8.     rcu_read_unlock();

  9.     proc_flush_task(p);

  10.     write_lock_irq(&tasklist_lock);
  11.     ptrace_release_task(p);
  12.     __exit_signal(p);

  13.     // 查看线程组是否已经没有线程存活了,如果已经空了,就通知线程组组长的父进程对组长进行收尸
  14.     zap_leader = 0;
  15.     leader = p->group_leader;
  16.     if (leader != p && thread_group_empty(leader)
  17.             && leader->exit_state == EXIT_ZOMBIE) {
  18.         zap_leader = do_notify_parent(leader, leader->exit_signal);
  19.         if (zap_leader)
  20.             leader->exit_state = EXIT_DEAD;
  21.     }

  22.     write_unlock_irq(&tasklist_lock);
  23.     release_thread(p); //释放线程资源,注意当前线程的task_struct在这里是不会释放的(引用计数不为0),因为目前还使用的它的进程上下文,schedule还需要他参与,它会在switch_to函数执行之后释放。
  24.     call_rcu(&p->rcu, delayed_put_task_struct);

  25.     p = leader;
  26.     if (unlikely(zap_leader))
  27.         goto repeat;
  28. }
    do_notify_parent函数大概实现如下:
点击(此处)折叠或打开
  1. bool do_notify_parent(struct task_struct *tsk, int sig)
  2. {
  3.     struct siginfo info;
  4.     unsigned long flags;
  5.     struct sighand_struct *psig;
  6.     bool autoreap = false;
  7.     cputime_t utime, stime;

  8.     if (sig != SIGCHLD) {
  9.         if (tsk->parent_exec_id != tsk->parent->self_exec_id)
  10.             sig = SIGCHLD;
  11.     }
  12.     // 初始化信号发送结构SIGCHLD
  13.     info.si_signo = sig;
  14.     info.si_errno = 0;
  15.     rcu_read_lock();
  16.     info.si_pid = task_pid_nr_ns(tsk, task_active_pid_ns(tsk->parent));
  17.     info.si_uid = from_kuid_munged(task_cred_xxx(tsk->parent, user_ns),
  18.                  task_uid(tsk));
  19.     rcu_read_unlock();

  20.     task_cputime(tsk, &utime, &stime);
  21.     info.si_utime = cputime_to_clock_t(utime + tsk->signal->utime);
  22.     info.si_stime = cputime_to_clock_t(stime + tsk->signal->stime);

  23.     info.si_status = tsk->exit_code & 0x7f;
  24.     if (tsk->exit_code & 0x80)
  25.         info.si_code = CLD_DUMPED;
  26.     else if (tsk->exit_code & 0x7f)
  27.         info.si_code = CLD_KILLED;
  28.     else {
  29.         info.si_code = CLD_EXITED;
  30.         info.si_status = tsk->exit_code >> 8;
  31.     }

  32.     psig = tsk->parent->sighand;
  33.     spin_lock_irqsave(&psig->siglock, flags);
  34.     if (!tsk->ptrace && sig == SIGCHLD &&
  35.      (psig->action[SIGCHLD-1].sa.sa_handler == SIG_IGN ||
  36.      (psig->action[SIGCHLD-1].sa.sa_flags & SA_NOCLDWAIT))) {
  37.         // 父进程说:它要为儿子收尸,所以autoreap = true;后面会设置为僵尸进程,如果父进程一直不收,则一直处理僵尸状态
  38.         autoreap = true;
  39.         if (psig->action[SIGCHLD-1].sa.sa_handler == SIG_IGN)
  40.             sig = 0;
  41.     }
  42.     // 给父进程发一个SIGCHLD信号,通知收尸
  43.     if (valid_signal(sig) && sig)
  44.         __group_send_sig_info(sig, &info, tsk->parent);
  45.     // 唤醒父进程
  46.     __wake_up_parent(tsk, tsk->parent);
  47.     spin_unlock_irqrestore(&psig->siglock, flags);

  48.     return autoreap;
  49. }
    总结:在linux当中,进程和线程表示方法是一样的,都是task_struct,因此,一个进程也是一个线程。每个进程都是它所在的线程组中的组长,凡是由它创建的线程(pthread_create)都会加入到它所在的线程组。因此,一但一个进程调用exit退出时,它首先要做的就是检查它所在的线程组里面是否还有其他线程,如果没有了,那么group_dead就为真,表示它所在的线程组将不复存在(毕竟它自己都死了)。然后回收该进程的资源,直到调用exit_notify,这个函数会检查当前进程是否还有子进程,如果有,那么这些子进程就成了孤儿进程,因此就调用了forget_original_parent函数为它的子进程寻觅新的父进程(这个父进程一般先从退出进程的兄弟线程中选,毕竟兄弟线程是共享资源的,如果这个退出线程没有兄弟线程,那么从就从它所在的pid空间选择1号进程来接管,通常它所在的pid空间就是init_ns,即init会成为新的父进程)。当然在forget_original_parent函数中找到了新的父进程还并没有结束,还会判断因为当前进程退出,是否会产生子进程所在的组产生孤儿进程组,如果产生了那么给线程组内的所有还有stop job的进程发送SIGHUP和SIGCONT信号,中间如果发现已经僵尸状态的或者可以回收资源的进程,都会加入到dead链表,由后面统一释放。好了,此后新的父进程已经有了,exit_notify函数会立马判断当前进程所在的进程组是否成为孤儿进程组,如果是,则也向进程组内的有stop job的进程发送SIGHUP和SIGCONT信号。最后,exit_notify函数中会判断,退出的进程是否为线程组组长,如果是,则还要判断线程组内是否还有成员,如果没有线程成员,那么就直接通知退出进程的父进程收尸;如果还有线程成员,那么这个进程就不通知它的父进程收尸,自己后期会成为僵尸进程,一直到它所在的线程组内所有线程退出后,才释放资源(函数实现在exit_notify->release_task)

    文中提到的switch_to和schedule将会在系统调度章节详细说明;文中提到到mm_struct相关的,将在虚拟地址部分讲到。

    通过《Linux内核之execve函数》和本章,我们知道进程的生命周期函数调用栈如下:


阅读(6815) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~