Linux内核设计与实现——进程管理

主要内容

  • 进程
  • 进程描述符及任务结构
  • 进程创建
  • 线程在linux中的实现
  • 进程终结

1. 进程

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

线程是进程中活动的对象,拥有独立的PC程序计数器,进程栈和一组进程寄存器,是内核调度的基本对象。

进程的两种虚拟机制:虚拟处理器和虚拟内存

相关函数:

  • fork(),系统调用从内核返回两次,一次回到父进程,一次回到新产生的子进程
  • exec(),创建新的地址空间并载入程序
  • exit() 退出执行
  • wait() , waitpid(),父进程等待子进程终结

2. 进程描述符及任务结构

双向链表的任务队列

  1. 分配进程描述符
    linux通过slab分配器分配task_struct结构,通过预先分配和重复使用,可以避免动态分配和释放带来的资源消耗。
    在进程的内核栈中,每个任务的thread_info结构在它内核栈的尾端分配,结构中task域存放的是指向该任务实际task_struct的指针。
  2. 进程描述符的存放
    pid,最大值默认为short int的最大值32768,可以修改pid_max文件来提高上限
  3. 进程状态


    Linux内核设计与实现——进程管理_第1张图片
    进程状态转化.png
  • TASK_RUNNING,运行——进程或者正在执行,或者在运行队列中等待执行
  • TASK_INTERRUPTIBLE,可中断——进程正在睡眠(被阻塞),等待某些条件的达成
  • TASK_UNINTERRUPTIBLE,不可中断
  • _TASK_TRACED,被其它进程跟踪的进程
  • _TASK_STOPPED,进程停止执行,如接收到SIGSTOP,SIGTINT等信号
  1. 设置当前进程状态
    set_task_state(task, state)
  2. 进程上下文
    当一个程序执行了系统调用或者触发了某个异常,它就陷入了内核空间,此时,我们呈内核“代表进程执行”并处于进程上下文中
  3. 进程家族树
    Unix系统的进程之间存在一个明显的集成关系,所有进程都是PID为1的init进程的后代
    进程关系:父子,兄弟,在进程描述符中存放,每个task_struct都有指向父进程、子进程的指针

3. 进程创建

linux中创建进程分两步,fork和exec

  • linux的fork()使用写时拷贝(copy-on-write)实现,因为有可能fork后是执行一个新的映像,父进程和子进程共享同一份拷贝,只有在需要写入的时候,才会被复制。因此,fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符

linux通过clone()系统调用do_fork(),do_fork()调用copy_process()函数,然后让进程开始运行,copy_process()完成的工作如下:

  1. 调用dup_task_struct()为新进程分配内核栈,task_struct等,其中的值与当前进程相同,此时,子进程与父进程的描述符也相同
  2. 检查创建子进程后,当前用户所拥有的进程数目没有超出给它分配的资源限制
  3. 子进程使自己与父进程区别开来。进程描述符内许多统计信息成员都要被清0或设为初始值
  4. 子进程状态设置为TASK_UNINTERRUPTIBLE,以保证不会投入运行
  5. 更新task_struct的flags成员,表明进程权限等
  6. 调用alloc_pid()为新进程分配一个有效的PID
  7. 根据传递给clone()的参数,copy_process()拷贝或共享打开的文件,文件系统信息,信号处理函数,进程地址空间和命名空间等
  8. 扫尾工作,返回指向子进程的指针

回到do_fork()函数后,内核会优先选择子进程首先执行,因为子进程通常会马上调用exec()函数,可以避免写时拷贝的额外开销。
创建进程的fork()函数实际上最终是调用clone()函数。

vfork()调用,不拷贝父进程的页表项,子进程作为父进程的一个单独的线程在它的地址空间里运行,父进程被阻塞直到子进程退出或执行exec(),子进程不能向地址空间写入。
现在for()引入了写时拷贝并且明确了子进程先执行,vfork()的好处就仅限于不执行父进程的页表项了。
而且如果exec()调用失败会怎样?

4. 线程在Linux中的实现

从内核的角度来说,并没有线程这个概念,Linux把所有的线程都当做进程来实现,线程仅仅被视为一个与其他进程共享某些资源的进程。
Windows在内核中提供了专门支持线程的机制。而对Linux来说,只是一种进程间共享资源的手段。

线程的创建和普通进程的创建类似,只不过调用clone()的时候需要传递额外参数标识

  1. 一个普通的fork():clone(SIGCHLD, 0)
  2. 创建线程:clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0) 使子进程和父进程共享地址空间,文件资源,文件描述符,信号处理程序等。

内核线程:

  • 内核线程没有独立的地址空间,只在内核空间运行,可以被调度或抢占

5. 进程终结

当一个进程终结时,内核必须释放它所占有的资源并告知父进程
终结任务依靠do_exit()来完成:

  1. 设置task_struct中的标识成员设置为PF_EXITING
  2. 调用del_timer_sync()删除内核定时器, 确保没有定时器在排队和运行
  3. 调用exit_mm()释放进程占用的mm_struct
  4. 调用sem__exit(),使进程离开等待IPC信号的队列
  5. 调用exit_files()和exit_fs(),释放进程占用的文件描述符和文件系统资源
  6. 把存放在task_struct的exit_code成员中的任务退出代码置为exit()提供的退出代码,或者去完成任何其他由内核机制规定的退出动作,退出代码存放在此供父进程随时检索
  7. 调用exit_notify()向父进程发送信号,给子进程重新找养父(init进程),并把进程状态设为EXIT_ZOMBIE
  8. do_exit()调用schedule()切换到新的进程,不会再被调度,do_exit()永不返回

至此,与进程相关联的所有资源都被释放掉了,进程处于(EXIT_ZONBIE退出状态)且不可运行,占用的内存仅剩内核栈、thread_info结构和tast_struct结构,此时进程存在的唯一目的就是向它的父进程提供信息。父进程检索到信息后,由进程所持有的剩余内存被释放,归还给系统。

wait()通过系统调用wait4()来实现,返回子进程的PID,调用该函数时提供的指针会包含子函数退出时的退出代码

  • 删除进程描述符,父进程调用release_task()
  • 如果没有父进程,子进程exit_notify()的时候,调用forget_original_parent() -> find_new_reaper()来执行寻父过程

你可能感兴趣的:(Linux内核设计与实现——进程管理)