进程管理

  1. 进程是处于执行期的程序,和相关资源的总称包括代码段,打开的文件,挂起的信号,内核内部数据,内存地址空间,多个执行线程的资源,数据段等。虚拟处理器和虚拟内存的机制让进程觉得自己独占处理器和该系统的所有内存资源。
  2. 同一个进程里的线程共享内存地址空间,是内核调度的最小单位。每个线程拥有自己的程序计数器,进程栈和一组进程寄存器。

Linux对进程(task)和线程不做区分,把线程作为特殊的进程进行处理。

  • 通常用fork()创建新的进程(父子进程),调用结束后,在返回点这个相同的位置,父进程恢复执行,子进程开始执行。从内核中返回两次,一次回到父进程,一次回到子进程。
  • 接着调用exec()函数创建新的地址空间,将程序载入其中。最终使用exit()推出执行,释放所有资源
  • 父进程可以通过wait4()查询子进程是否终结,拥有等待特定进程执行完毕的能力。进程推出后被设置为僵死状态,直到他的父进程调用wait()或者waitpid()为止。

进程描述符和任务结构

  1. task list存放内存队列,双向链表,每个节点类型为task_struct(process descriptor),定义在<linux/sched.h>。
  2. task_struct在32位机器上1.7KB,包含打开的文件,进程的地址空间,挂起的状态和其他信息。
  3. Linux通过slab分配器分配task_struct结构,位置在进程内核栈尾端,原因1是让寄存器比较弱的硬件可以通过栈指针计算出他的位置,不用额外的寄存器记录(有的硬件就可以直接记录PowerPC);原因2是汇编代码中计算偏移量容易。+thread_info结构x86,中间包含task_struct指针。
  4. PID用来标识每个进程,pid_t类型(int实际上),默认大小32768,2^15(short int),最大值在<linux/threads.h>中配置,即允许最大的进程数量,可以通过/proc/sys/kernel/pid_max来提高。
  5. current宏可以找到当前进程task_struct,但实现方式决定于硬件体系x86需要借助thread_info结构,current_thread_info()->task;
  6. 进程状态包括:

    • TASK_RUNNING运行时

      • 可执行的
      • 正在执行
      • 在运行队列中等待执行的
      • 进程在用户空间中执行的唯一可能状态
    • TASK_INTERRUPTIBLE 可中断,

      • 进程正在睡眠(阻塞),待条件达成或者收到信号进入运行
    • TASK_UNINTERRUPTIBLE 不可中断

      • 进程在等待时不受干扰或者等待事件很快会发生时出现
      • ps 标记为D而不能被杀死的进程的原因,kill信号不接受
    • _TASK_TRACED 被其他进程追踪,ptrace
    • _TASK_STOPPED 没有投入运行也不能运行,发生在SIGINT,SIGTSTP,SIGTOU等信号出现
  7. 可以通过set_task_state(task, state);设置进程状态;set_current_state(state)和set_task_state(current, state)相同<linux/sched.h>
  8. 关于内核中的进程上下文: 如果用户进程是通过系统调用或者触发异常陷入的内核空间,内核就代表进程执行并处于进程的上下文,在此上下文上,current宏定义是有效的,除非在此间隙有更高优先级的进程需要执行并由调度器作出了相应的调整,否则在内核退出的时候,程序恢复在用户空间中执行。
  9. 进程间的关系。Linux所有进程都是init进程的子进程。该进程读取系统的初始化脚本,并执行其他的相关程序,并最终完成系统启动的整个过程。进程必有一个父进程,可以有一个或者多个子进程,同一个父进程的子进程被称为兄弟进程。这些关系被放在进程描述符中:current->parent, current->children子进程链表;next_task(task), prev_task(task)来分别获区前一个进程和后一个进程。for_each_process(task)获取每一个进程。

    进程的创建

  10. fork() + exec()

    • fork()通过拷贝在新的地址空间内创建进程,
    • 子进程和父进程区别在于PID和某些资源及统计量上;挂起的信号没有必要被继承。
    • 写时拷贝(COW),推迟或者免除拷贝数据的技术,内核并不复制整个进程地址空间,而让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。地址空间上页的拷贝被推迟到实际发生写入的时候才进行。
    • 在页根本不会被写入的情况下(fork后直接调用exec),他们无需复制
    • 开销在于复制父进程的页表以及给子进程创建唯一的进程描述符
    • 底层实现是clone(),参数表明父子需要共享的资源->do_fork() (kernel/fork.c) -> copy_process()

      • 调用dup_task_struct(),为新进程创建一个内核栈、thread_info结构和task_struct,这些值父子进程暂时相同
      • 子进程对描述符内部分成员清0或者设为初始化值(主要是统计信息)
      • 子进程为Task_UNINTERRUPTIBLE,以保证他不会被投入云南行
      • 调用copy_flags()更新flags成员: PF_SUPERPRIV(超级用户权限)=0;PF_FORKNOEXE(表明未被exec)=1
      • 调用alloc_pid()生成新的PID
      • copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。
      • 扫尾并返回子进程指针。
    • do_fork()获得子进程指针后,子进程呗唤醒并投入运行,内核选子进程先执行,一般子进程都会马上调用exec()函数,避免写时拷贝的开销
    • exec()读入可执行文件,并执行

线程的实现

Linux本身在内核中不对线程和进程进行划分,线程只是特殊的进程,与其他进程共享某些资源。每个线程都有自己的task_struct.

创建线程

  1. clone()的时候传递的一些参数标志来指明需要共享的资源:
    clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
    通过调用fork()差不多,只不过共享了内存空间,文件系统,文件描述符,信号处理程序,两者成为线程关系。
    普通的fork()调用是: clone(SIGCHLD, 0)
    <linux/sched.h>

    参数标志含义
    CLONE_FILES父子进程共享打开的文件
    CLONE_FS父子进程共享文件系统信息
    CLONE_IDLETASK将PID设置为0只给idle进程使用
    CLONE_NEWNS为子进程创建新的命名空间
    CLONE_PARENT指定子进程与父进程拥有同一个父进程
    CLONE_PTRACE继续调试子程序
    CLONE_SETTID将TID回写到用户空间
    CLONE_SETTLS为子进程创建新的TLS(thread-local storage)
    CLONE_SIGHAND父进程共享信号处理函数及被阻断的信号
    CLONE_SYSVSEM父子进程共享System V SEM_UNDO含义
    CLONE_THREAD父子进程放入相同的线程组
    CLONE_VFORK调用vfork()
    CLONE_UNTRACED防止跟踪进程在子进程强行执行CLONE_PTRACE
    CLONE_STOP以TASK_STOPPED状态开始进程
    CLONE_CHILD_CLEARTID清除子进程的TID
    CLONE_CHILD_SETTID设置子进程的TID
    CLONE_PARENT_SETTID设置父进程的TID
    CLONE_VM父子进程共享内存空间

内核线程

  1. 内核在后台操作使用的线程
  2. 并没有独立的地址空间
  3. 只在内核空间运行,只能由其他内核线程创建(kthreadd初始内核线程)
  4. 可以被抢占,调度
  5. 类似于flush和ksofirqd
  6. 创建:<linux/kthread.h>
    struct task_struct *kthread_create(int (*threadfn)(void *data), void *data, const char namefmt[], ...)
    新的kthread会执行threadfn函数,参数为data,名字被命名为namefmt,新创建的进程处于不可执行状态,需要wake_up_process()来明确唤醒他
    struct task_struct *kthread_run(int (*threadfn), void *data, const char namefmt[],...)
    创建并让他运行,宏函数,调用create+wake_up_process
  7. 创建后一直运行直到do_exit()退出,或者其他调用kthread_stop()退出
    int kthread_stop(struct task_struct *k)

进程终结

  1. 当进程终结时,内核必须释放他所占有的资源并告知其父进程。
  2. 进程的析构是自身引起的,发生在进程主动调用exit()系统调用(显式或隐式)时;或者被动收到它不能忽略的信号或者异常。do_exit() (kernel/exit.c)

    • tast_struct标志成员变量设置为PF_EXITING
    • 调用del_timer_sync()删除任意一个内核定时器,根据返回结果,确保没有定时器在排队或者没有定时处理程序在运行
    • 如果BSD的进程记账功能是开启的话,do_exit()调用acct_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_ZOMBIE.
    • do_exit()调用schedule()切换到新的进程,永不返回。
    • 他处于不可运行状态,没有内存空间,只保留内核栈,thread_info结构和task struct结构,给父进程提供信息,父进程收到后,通知内核他不需要该信息,释放所有资源(task struct)。

      • release_task()调用:

        • __exit_signal()释放僵死进程的所有资源,并进行最终统计和记录;
        • 调用_unhash_process(),调用detach_pid()从pidhash中删除该进程,同时从任务列表中删除。
        • 如果他已经是该线程组中最后一个进程,并且领头进程已经死掉,则通知僵死的零头进程的父进程。
        • put_task_struct()释放进程内核栈和thread_info所占页,释放slab高速缓存。

孤儿进程

  1. 保证当父进程在子进程之前退出时,需要给子进程找一个养父(do_exit()中调用exit_notify()->forget_original_parent() -> find_new_reaper());
  2. 在当前线程组中找到养父,如果不行就是init进程.//exit.c

    static struct task_struct *find_alive_thread(struct task_struct *p)
    {
     struct task_struct *t;
    
     for_each_thread(p, t) {
         if (!(t->flags & PF_EXITING))
             return t;
     }
     return NULL;
    }
    
    
    /*
     * When we die, we re-parent all our children, and try to:
     * 1. give them to another thread in our thread group, if such a member exists
     * 2. give it to the first ancestor process which prctl'd itself as a
     *    child_subreaper for its children (like a service manager)
     * 3. give it to the init process (PID 1) in our pid namespace
     */
    static struct task_struct *find_new_reaper(struct task_struct *father,
                        struct task_struct *child_reaper)
    {
     struct task_struct *thread, *reaper;
    
     thread = find_alive_thread(father);
     if (thread)
         return thread;
    
     if (father->signal->has_child_subreaper) {
         unsigned int ns_level = task_pid(father)->level;
         /*
          * Find the first ->is_child_subreaper ancestor in our pid_ns.
          * We can't check reaper != child_reaper to ensure we do not
          * cross the namespaces, the exiting parent could be injected
          * by setns() + fork().
          * We check pid->level, this is slightly more efficient than
          * task_active_pid_ns(reaper) != task_active_pid_ns(father).
          */
         for (reaper = father->real_parent;
              task_pid(reaper)->level == ns_level;
              reaper = reaper->real_parent) {
             if (reaper == &init_task)
                 break;
             if (!reaper->signal->is_child_subreaper)
                 continue;
             thread = find_alive_thread(reaper);
             if (thread)
                 return thread;
         }
     }
    
     return child_reaper;
    }
    
    
    
    static struct task_struct *find_child_reaper(struct task_struct *father,
                         struct list_head *dead)
     __releases(&tasklist_lock)
     __acquires(&tasklist_lock)
    {
     struct pid_namespace *pid_ns = task_active_pid_ns(father);
     struct task_struct *reaper = pid_ns->child_reaper;
     struct task_struct *p, *n;
    
     if (likely(reaper != father))
         return reaper;
    
     reaper = find_alive_thread(father);
     if (reaper) {
         pid_ns->child_reaper = reaper;
         return reaper;
     }
    
     write_unlock_irq(&tasklist_lock);
    
     list_for_each_entry_safe(p, n, dead, ptrace_entry) {
         list_del_init(&p->ptrace_entry);
         release_task(p);
     }
    
     zap_pid_ns_processes(pid_ns);
     write_lock_irq(&tasklist_lock);
    
     return father;
    }

    然后调用ptrace_exit_finish()寻找新的父

    /*
     * Detach all tasks we were using ptrace on. Called with tasklist held
     * for writing.
     */
    void exit_ptrace(struct task_struct *tracer, struct list_head *dead)
    {
     struct task_struct *p, *n;
    
     list_for_each_entry_safe(p, n, &tracer->ptraced, ptrace_entry) {
         if (unlikely(p->ptrace & PT_EXITKILL))
             send_sig_info(SIGKILL, SEND_SIG_PRIV, p);
    
         if (__ptrace_detach(tracer, p))
             list_add(&p->ptrace_entry, dead);
     }
    }

    遍历子进程链表和ptrace子进程链表,为每个子进程设置新的父进程;以前需要遍历全部进程,现在只用ptrace的进程链表。
    init例行调用wait检查他的子进程,清除僵尸进程。


唠叨的水龙头
1 声望0 粉丝

引用和评论

0 条评论