读书笔记:《Linux内核设计与实现》第三章 —— “进程管理”

目录

3.1进程

3.2进程描述符及任务结构

3.3进程创建

3.4线程在Linux中的实现

3.5进程终结


    在第三章中,主要考察了操作系统的核心概念——进程。以及Linux如何存放和表示进程(用task_struct和thread_info),如何创建进程(通过fork(),实际上最终是clone()),如何把新的执行映像装入到地址空间(通过exec()系统调用族),如何表示进程的层次关系,父进程又是如何收集子进程信息(通过wait()系统调用族),以及进程最终如何消亡(强制或自愿的调用exit())。

3.1进程

     进程就是处于执行期的程序,但进程并不仅仅局限于一段可执行代码程序,通常还要包含其他资源,例如打开的文件,挂起的信号,内核内部数据,处理器状态,一个或多个具有内存映射的内存地址空间等。实际上,进程就是正在执行的程序代码的实时结果。 【进程是处于执行期的程序以及相关资源的总称

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

     Linux系统的线程实现非常特别:它对于线程和进程并不特别区分,对Linux而言,线程只不过是一种特殊的进程罢了。

     进程提供两种虚拟机制:虚拟处理器和虚拟内存 。虚拟处理器是尽管可能有很多进程正在分享一个处理器,但虚拟处理器给进程一种假象,让这些进程觉得自己在独享处理器;虚拟内存是让进程在分配和管理内存时觉得自己拥有整个系统的所有内存资源;But,在线程之间可以共享虚拟内存,但每个都拥有各自的虚拟处理器。

     在Linux系统中,通常是调用fork()函数来复制一个现有进程来创建一个全新的进程;调用fork()的进程是父进程,新产生的进程是子进程,在调用结束后,在返回点这个相同位置上,父进程恢复执行,子进程开始执行。fork()系统调用从内核返回两次:一次回到父进程,另一次回到新产生的子进程;

    在最后,程序通过exit()系统调用退出执行,这个函数会终结进程并且释放掉占用的所有资源。父进程可以通过exit4()系统调用查询子进程是否终结,这也是让进程拥有了等待特定进程执行完毕的能力。进程退出执行后被设置为僵死状态,直到它的父进程调用wait()或waitpid()为止。

3.2进程描述符及任务结构

    内核把进程的列表放在任务队列(task list)的双向循环链表中。链表的每一项类型都是task_struct、称为进程描述符【1.7KB】的结构,定义在<Linux/sched.h>中。进程描述符包含了一个进程的所有信息:进程打开的文件,进程的地址空间,挂起的信号,进程的状态等。

                                                 

    内核通过一个唯一的进程标识值或PID来标识每个进程。PID是一个数,表示为pid_t类型,实际上就是int类型。为了让老版本的Unix和Linux兼容,PID的最大值默认设置32768(short int短整型的最大值),但并不是绝对的,可以通过<Linux/threads.h>来进行设置。内核把每个进程的PID存放到各自的进程描述符中。

    这个最大值实际上就是系统允许同时存在的进程的最大数目。尽管32768对于一般的桌面系统足够用了,但是大型服务器可能需要更多的进程。【值越小,转一圈的速度也就更加快!】

    在内核中,访问任务需要获得指向task_struct的指针。实际上,内核中大部分处理进程的代码都是直接通过task_struct进行的,我们可以通过current宏来查找到当前进程的进程描述符。这个查找速度也是很关键的,不同的硬件体系结构实现宏的方式也不同,有充足寄存器的结构就可以拿出一个专门的寄存器来存放指向当前进程task_struct的指针,来加快访问速度;那么x86体系寄存器体系并不富裕,那么就会选择在内核栈的尾端创建thread_info结构,通过计算偏移间接的查找task_struct结构。

current宏,是一个全局指针,指向当前进程的struct task_struct结构体,即表示当前进程。

current->pid就能得到当前进程的pid;
current-comm就能得到当前进程的名称。

     进程描述符中的state域描述了进程的当前状态,系统中的每个进程必然处于以下五种进程之一:

  1. TASK_RUNNING (运行)—— 进程是可执行的;它正在执行或者在运行队列中等待执行。 
  2. TASK_INTERRUPTIBLE(可中断)—— 进程正在睡眠(被阻塞),等待某些条件的达成,一旦条件实现,就可以投入运行,或者是接收到信号而被提前唤醒投入运行;
  3. TASK_UNINTERRUPTIBLE(不可中断)—— 除了就算是接收到信号也不会被唤醒投入运行,状态类似可中断;
  4. _TASK_TRACED—— 其他进程跟踪的进程,例如ptrace对调试程序进行跟踪。
  5. _TASK_STOPPED(停止)—— 进程没有投入运行也不能投入运行;【通常这种状态发生在接收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号的时候,此外在调试期间接收到任何信号,都会使进程进入到这种状态】

    【可以通过set_task_state函数来设置进程状态】

     所有的进程都是PID为1的init进程的后代,每个进程必有一个父进程,相应的每个进程也可以拥有零个或多个子进程。每个task_struct都包含一个指向其父进程task_struct叫做parent的指针,还包含一个称为children的子进程链表。

对于当前进程,可以通过以下步骤获得父进程的进程描述符:

struct task_struct;
*my_parent = current->parent;

so,我们可以通过这种继承体系从系统的任何一个进程出发查找到任意指定进程
//但是这也只是一个理论,这个过程消耗非常大。

 3.3进程创建

      许多其他的操作系统都提供了产生spawn进程的机制【spawn函数可以加载并运行称为子进程的其它文件】,首先在新的地址空间创建进程,读入可执行文件,最后开始执行。那么在Linux是把上述步骤分解到两个单独的函数中去执行:fork()和execv()。首先fork()通过拷贝当前进程来创建一个新进程。父子进程之间的区别仅仅在于PID(每个进程唯一),PPID(父进程的进程号,子进程将其设置为被拷贝进程的PID)和某些资源的统计量(例如挂起的信号就没有继承的必要);exec()函数读取可执行文件并将其载入到地址空间开始运行。

    父子进程之间的拷贝是写时拷贝,写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,让各个进程拥有各自的拷贝,在此之前,只是以只读的方式共享。fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。但是写时拷贝技术使地址空间上的页的拷贝被推迟到了实际写入的时候才进行,在页表根本不会被写入的情况下,就不需要进行复制。【例如,fork()之后立即调用exec()】

    Linux系统其实是同过clone()系统调用来实现fork()。fork()、vfork()和_clone()可函数都需要根据各自需要的参数标志;来调用clone()。然后由clone()来调用do_fork()。

                        

    vfork()除了不拷贝父进程的页表项外,vfork()和fork()的功能相同。子进程作为父进程的一个单独线程在它的地址空间里运行,父进程被阻塞,直到子进程退出或执行exec()。在fork()引入了写时拷贝并且明确了子进程先执行之后,vfork()的优点也就仅限于不拷贝父进程的页表项了。【最好不要使用vfork(),因为假如exec()失败,父进程会被一直阻塞下去!!!】

                            

3.4线程在Linux中的实现

     线程其实是一种抽象概念,提供了同一进程内资源共享。从内核角度来看,其实并没有线程这个概念,所有的线程都当作进程来实现,仅仅被看作是一个与其他进程共享某些资源的进程。每个线程都拥有唯一的task_struct,所以在内核中,线程看起来就像是一个普通的进程。

     线程也被叫做“轻量级进程”,相对于重量级进程,线程被抽象成一种耗费较少资源,运行迅速的执行单元。对于Linux而言,这也只是一种进程间共享资源的手段。【因为Linux的进程本身就够轻量级】。举个例子 ,假如我们有一个包含四个线程的进程,那么就有一个包含指向四个不同线程的指针的进程描述符,这个描述符负责描述地址空间、打开的文件这种共享资源。线程本身再去描述它独占的资源。Linux仅仅创建四个进程并且分配四个task_struct结构体,建立这四个进程时候指定他们共享的资源,这种做法看起来就比较明了。

      线程的创建和普通进程的创建类似,只不过在调用clone()的时候需要传递一些参数来指明需要共享的资源。传递给clone()的参数标志了新创建进程的行为方式和父子进程之间共享的资源种类。

    内核线程是在后台进行操作,没有独立的地址空间只能在内核空间运行。【eg:flush刷新缓冲区】 

 3.5进程终结

      一个进程的终结,内核必须释放它所占有的资源,并且告诉父进程。一般而言,进程的析构是由自身引起的,它发生在进程调用exi()之后。大部分任务都由do_exit()完成。

                                              

      在调用了do_exit()之后,尽管线程已经僵死的不能再运行了,但是系统还是保留了它的文件描述符,这样就可以让系统有办法在子进程终结后还可以获取到一些信息。所以,进程终结时的清理工作和进程描述符的删除被分开执行。在父进程获得已终结子进程信息后,或者通知内核不关注那些信息后,子进程的task_struct()才可以被释放。

                              

       如果父进程在子进程之前退出,必须有机制来保证子进程还可以找到一个新父亲,否则这些孤儿进程就会在退出时永远处于僵死状态,白白的耗费内存。那么如何解决这个问题呢?可以选择给子进程在当前线程组内找一个线程作为父亲,如果不行,就让init来接管这个进程。寻找新父亲的过程其实就是遍历子进程链表和ptrace子进程链表,给每个子进程设置新的父进程。【ptrace子进程链表其实就是在一个单独的被ptrace跟踪的子进程链表中搜索相关的兄弟进程】—— 用这两个表减少了遍历带来的消耗

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值