关于内核:读书笔记Linux内核设计与实现3

7次阅读

共计 6931 个字符,预计需要花费 18 分钟才能阅读完成。

过程治理

  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 查看他的子过程,革除僵尸过程。

正文完
 0