Linux内核学习之进程和线程初探,进程管理

图1 进程描述符及任务队列

进程的两种虚拟机制

  1. 虚拟处理器:每个线程独有,不能共享
  2. 虚拟内存:同一个进程中的线程可以共享

1.3 进程PID

Linux的内核使用PID来对进程进行唯一标识。PID是pid_t的隐含类型,PID的值受到<linux/threads.h>头文件中规定的最大值的限制,但是为了和传统的Unix操作系统兼容,PID会被默认设置为32768即short int短整型的最大值。PID的最大值是系统中允许同时存在的进程的最大数目。PID 的最大值可以通过/proc/sys/kernel/pid_max来修改。

    传统的 fork() 系统调用直接把所有的资源复制给新创建的进程。这种实现过于简单且效率低下,因为它拷贝的数据也许并不共享。Linux 的 fork() 使用写时拷贝页实现,写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是说,资源的复制只有在需要写入的时候才会进行,在此之前,只是以只读的方式共享。这种技术使得地址空间上的页的拷贝被推迟到实际发生写入的时候才进行。在页根本不会被写入的情况下,它们就无须复制了。

进程描述符及任务结构

  • 任务队列:存放进程列表的双向循环链表
  • task_struct:进程描述符,包含一个具体进程的所有信息。2.6以后的版本通过slab动态生成task_Linux内核学习之进程和线程初探,进程管理。struct。
  • thread_info:线程描述符,

1 进程

进程指的是处于执行期的程序。但是需要注意的是进程并不仅仅包括一段可执行程序的代码,它同时还包括其他资源,例如打开的文件,挂起的信号,内核内部数据,处理器状态,具有内存映射的地址空间和执行线程以及数据段等。

set_task_state(task,state);        // 将进程task的状态设置为 state

PID

唯一的进程标志值。int类型,为了与老版本的Unix和Linux兼容,PID的最大值默认设置为32768,这个值最大可增加到400万。进程的PID存放在进程描述符中。

2 进程创建

    Linux实现线程的机制很独特。从内核角度来说,它并没有线程这个概念。Linux把所有的线程都当做进程来实现。内核并没有准备特别的调度算法或是定义特别的数据结构来表征线程。相反,线程仅仅被视为一个与其他进程共享某些资源的进程。每个线程都拥有唯一隶属于自己的 task_struct ,所以在内核中,它看起来就像是一个普通的进程。

进程状态

进程描述符中的state域记录进程当前的状态,进程一共有五中状态,分别为:

  • TASK_RUNNING 运行
  • TASK_INTERRUPTIBLE 可中断
  • TASK_UNINTERRUPTIBLE 不可中断
  • __TASK_TRACED 被其他进程跟踪的进程
  • __TASK_STOPPED 进程停止执行

2.3 进程终结

进程终结时,内核必须要释放他所占用的资源,然后通知父进程。进程的析构发生在exit()系统调用时,可以是显式的,也可以是隐式的,例如从某个程序的主函数返回(对于C语言来说其实会在main()函数的返回点后面设置exit()代码)。当进程收到不能处理但是又不能忽视的信号或者出现异常时,也可能会被动终结。但是进程在终结是,大部分还是会调用do_exit()完成(在kernel/exit.c中定义)。
(1) 将task_struct中的标志成员设置为PF_EXITING
(2) 调用del_timer_sync()删除任意内核定时器。根据返回的结果确认没有任何定时器在排队,同时也没有任何定时器处理程序在运行。
(3) 若开启了BSD的进程记账功能,那么还需要调用acct_update_integrals()来输出记账信息
(4) 调用exit_mm()释放进程占用的mm_struct,若是没有其他进程使用这个地址空间,那么就彻底释放此地址空间
(5) 调用sem_exit()函数,若进程排队等待IPC信号,则离开队列
(6) 调用exit_file()exit_fs(),分别递减文件描述符、文件系统数据的引用计数。若释放后引用计数为0,则直接释放。
(7) 将存放在task_struct的exit_code成员中的任务退出代码置为由exit()提供的任务退出代码,或者完成任何其他由内核机制规定的退出动作。退出代码的存放是为了供父进程检索
(8) 调用exit_notify()函数向自己的父进程发送信号,并且给自己的子进程重新寻找养父,养父为线程组中的其他线程或者为init进程,然后将进程状态置为EXIT_ZOMBLE
(9) do_exit()调用schedule()切换到新进程。这是do_exit()执行的最后代码,退出后就不再返回。

     在调用 do_exit() 之后,尽管线程已经僵死不能再运行了,但是系统还是保留了它的进程描述符。在父进程获得已终结的子进程的信息后,或者通知内核它并不关注那些信息后,子进程的 task_struct 结构才被释放。调用 release_task() 来释放进程描述符。

进程终结

当一个进程终结时必须释放它所占有的资源。进程主动终结发生在进程调用exit()系统调用时,当然它还有可能被动终结。

  • 删除进程描述符:在调用do_exit()之后,尽管线程已经僵死不能再运行了,但系统还保留了它的进程描述符,在父进程获得已终结的子进程的信息或通知内核它不关注那些信息后,子进程的task_struct结构才释放。
  • 孤儿进程造成的进退维谷:由于进程退出时需要父进程通知父进程释放子进程的task_struct,如果一个进程找不到父进程就会在退出时永远处于僵死状态。因此要在父进程退出时为每一个子进程找到一个新的父亲,方法是给子进程在当前线程组内找一个线程作为父亲,如果不行就让init做它们的父进程。

3 线程

线程是指在进程中活动的对象,相对而言,线程仅仅局限在进程内部,线程拥有的资源远远比进程小,仅仅包括独立的程序计数器和进程栈以及一组进程寄存器。在其他操作系统中进程和线程的概念往往会被严格区分,但是对于Linux操作系统内核而言,它对线程和进程并不进行区分,线程通常被视为一个与其他进程共享某些资源的进程。每个线程都拥有自己的task_struct,所以线程在Linux内核中也被视为一个进程,这是和其他操作系统截然不同的。
线程的创建和进程是类似的但是在调用clone()的时候,会传递一些特殊的标志位,例如CLONE_VMCLONE_FSCLONE_FILESCLONE_SIGHAND,这些值都是由下表定义的。

图片 1

图片 2

clone()参数标志

内核很多时候还需要在后台执行一些操作,这些都是由内核线程(kernel thread)完成。内核线程独立于内核进程运行,同时内核线程没有独立的地址空间,并且不会切换到用户空间,其他和普通线程一样,没有区别。
内核线程一般是自动从内核进程中衍生而出,同样内核线程也是通过clone()系统调用实现,并且需要调用wake_up_process()函数来进行明确地唤醒。kthread_run()可以完成线程的唤醒和运行,但是本质上只是调用了kthread_create()wake_up_process()。内核线程可以使用do_exit()函数退出,也可以由内核其他部分调用kthread_stop()函数来进行退出。

  1)进程描述符 

线程实现

在Linux内核中线程看起来就是一个普通的进程,只是和其它一些进程共享某些资源,如地址空间。

  1. 创建线程同样使用clone实现,只是需要传递一些参数标志来指明需要共享的资源:clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
  2. 内核线程没有独立的地址空间,只在内核空间运行,不切换到用户空间上去,只能由内核线程创建。

1.1 进程描述符

一个操作系统如果想管理好进程,那么操作系统就需要这个进程的所有信息,Linux内核成功抽象了进程这一概念,然后使用task_struct即进程描述符来对进程进行管理,同时内核使用双向链表(即任务队列)对进程描述符进行了相应的组织。(task_struct结构体定义在<linux/sched.h>)。

图片 3

task_struct和任务队列

task_struct在32位计算机中占有1.7KB。包含一个进程所有的信息,包含打开的文件,进程地址空间,挂起的信号,进程状态等,具体可以参照在Linux内核代码中定义的task_struct结构体代码。Linux在分配进程描述符时,使用了slab机制(可以查看进程创建一节)。当进程描述符task_struct分配完毕之后,需要对其进行存放。

三、进程创建

进程上下文

通常进程的代码在用户空间执行,当执行了系统调用或触发了某个异常时,它就陷入了内核空间。此时,我们称内核处于进程上下文中。

4 进程和线程的区别

对于Linux内核而言,进程和线程没有区别。对于Linux内核而言,并没有对线程进行特殊处理,而是将线程与进程同等对待,这与其他操作系统完全不同。其他操作系统都提供了专门的机制去实现多线程机制,由于Linux强大轻便快速的进程创建手段,所以Linux仅仅将线程看作是进程共享了进程资源的多个进程,对于Linux内核来说创建线程等价于创建一个进程。通过Linux内核可以得知,一个进程的多线程其实只是共享了很多资源,例如地址空间等。由此产生了“Linux没有多线程机制“”这一说法,但是本质上来说,并不是Linux没有多线程机制,只是其实现方式和其他操作系统不同而已。

这是个人在阅读《Linux内核设计与实现》时候的一点心得,里面加入了一些自己关于操作系统的理解,对自己的现有的知识进行梳理,如有错误敬请指正。

    进程是处于执行期的程序,但是并不仅仅局限于一段可执行程序代码。通常,进程还要包含其他资源,像打开的文件,挂起的信号,内核内部数据,处理器状态,一个或多个具有内存映射的内存地址空间及一个或多个执行线程,当然还包括用来存放全局变量的数据段等。在Linux内核中,进程也通常叫做任务

进程创建

  1. 写时拷贝,父子进程共享同一个地址空间,将页的拷贝推迟到实际发生写入时才进行。这个优化可以避免创建进程时拷贝大量不被使用的数据。
  2. 在进程中调用fork()会通过复制一个现有进程来创建一个新进程,调用fork()的进程是父进程,创建的进程是子进程。fork()函数从内核返回两次,一次是返回父进程,另一次返回子进程。Linux通过 clone(SIGCHLD, 0);系统调用实现fork()。
  3. vfork() 不拷贝父进程的页表项,其它与fork功能相同。系统实现:clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0);
  4. exec()这组函数可以创建新的地址空间,并把新的程序载入其中。

2.1 创建过程

在Linux进程创建不同于其他操作系统,Linux操作系统提供了两个单独的函数完成进程的创建工作。其中fork()函数通过拷贝完成子进程的创建,子进程会完全拷贝父进程中的绝大多数资源,(除了PID和PPID,以及一些敏感资源和统计量)。然后在使用exec()函数完成可执行文件的读取,并且将其载入地址空间运行。而其他操作系统一般只使用一个函数完成上述的两步操作。

fork()函数是通过clone()系统调用实现的。此调用会通过一系列参数标志指明父子进程需要共享的资源。库函数根据参数标志调用clone()clone()调用do_fork()函数do_fork()函数在kernel/fork.c中定义,并且完成了创建中的大部分工作。然后该函数会去调用copy_process()函数copy_process()函数完成了下述工作:
1) 调用duo_task_struct()函数为新进程创建内核栈thread_info、和task_struct,但是这些值都和当前进程的相同,只是一份简单的复制
2) 检查当前用户的进程总数是否超过限制
3) 将进程描述符中关于目前进程的统计信息清零,使得子进程和父进程能够进行区分
4) 将子进程状态设为TASK_UNINTERRUPTIBLE,使其不能运行
5) 调用copy_flag()函数更新task_structflags成员。将超级用户权限标志符PF_SUPERPRIV清零,然后将进程未调用exec()函数标志位PF_FORKNOEXEC置位。
6) 调用alloc_pid()为新进程分配一个有效PID
7) 根据传递给clone()的参数标志,该函数(即copy_process()函数)拷贝或者共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间。
8) 扫尾,然后返回一个指向子进程的指针

当然还有其他形式的fork()函数实现方式。例如vfork()函数功能和fork()函数相同,但是vfork()函数不会拷贝父进程的页表项。vfork()生成的子进程作为一个单独的线程在其地址空间内运行,父进程会被阻塞,直到子进程退出或者调用exec()函数,子进程不允许向地址空间内写入数据。但是在使用了写时拷贝技术之后,这一项技术其实已经无关紧要了。

图片 4

定义

进程就是处于执行期的程序。实际上,进程就是正在执行代码的实际结果。
线程是在进程中活动的对象,每个线程都拥有独立的程序计数器,进程栈以及一组进程寄存器。内核的调度对象是线程,而不是
进程。

1.2 内核进程操作

对于一个进程来说,在内存中会分配一段内存空间,一般来说这个空间为1或者2个页,这个内存空间就是进程的内核栈。在进程内核栈的栈底有一个结构体变量为thread_info,这个结构体变量中包含了一个指向该进程描述符task_struct的指针,这个变量的存在,可以使内核快速地获得某一个进程的进程描述符,从而提高响应速度。在x86体系结构中,内核中的current宏就是通过对于这个结构体的访问来实现的,而在其他寄存器丰富的体系结构中看,可能会没有使用thread_info结构体,而是直接使用某一个寄存器来完成例如PPC体系结构。

/*x86中thread_info的定义*/
struct thread_info {
    struct task_struct  *task;      /* main task structure */
    struct exec_domain  *exec_domain;   /* execution domain */
    unsigned long       flags;      /* low level flags */
    unsigned long       status;     /* thread-synchronous flags */
    __u32           cpu;        /* current CPU */
    int         preempt_count;  /* 0 => preemptable, <0 => BUG */

    mm_segment_t        addr_limit; /* thread address space:
                         * 0-0xBFFFFFFF for user-thead
                         * 0-0xFFFFFFFF for kernel-thread
                         */
    struct restart_block    restart_block;
    __u8            supervisor_stack[0];
};

    执行线程,简称线程,是在进程中活动的对象。每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程,而不是进程。在传统的UNIX系统中,一个进程只包含一个线程,但在现在的系统中,包含多个线程的多线程程序司空见惯。在Linux系统中,线程和进程并不特别区分,对Linux而言,线程是一种特殊的进程

2.2 进程创建优化

由于进程描述符task_struct是一个在进程创建时必须的数据结构,所以进程的创建速度可以通过加速进程描述符的创建来提高,有鉴于此,内核使用了slab机制来对其进行处理。所谓slab机制,就是对于频繁被使用的数据结构,会进行缓存,而不是使用完毕之后直接进行释放。这样做的好处是,如果需要频繁创建某一数据结构变量,只是直接使用即可,而不需要进行内存的申请,使用完毕也不需要释放,大大减少了分配内存和回收内存的时间。使用slab机制后,进程描述符可以被快速地建立,同时进程销毁时也不需要去进行进程描述符的内存释放。

当然Linux内核在其他方面也使用了加快进程创建的方法。上面讲到,Linux创建进程使用fork()函数来完成,而fork()函数又使用clone()系统调用来实现,但是需要注意的是,创建一个新进程时,Linux内核加入了写时拷贝机制来加速进程的创建,而不是完整地对进程所有内容进行简单的复制。所谓写时拷贝就是在新进程创建时,子进程和父进程共享一个进程地址空间拷贝,当子进程或者父进程对这个拷贝执行写入操作后,数据才会被复制,然后进行各自的修改,所以资源在未进行写入时,以只读方式共享。这种写时拷贝的方式,将进程的创建开销从子进程对父进程资源的大量复制,简化为复制父进程的页表和子进程唯一进程描述符的创建

    Linux通过slab分配器分配 task_struct 结构,这样能达到对象复用和缓存着色的目的,为了找到 task_struct,只需在栈底(对于向下增长的栈)或栈顶(对于向上增长的栈)创建一个新的结构 struct thread_info,该结构存放着指向任务实际 task_struct 的指针。结构的定义如下:

1.4 进程家族树

Linux和Unix系统一样,进程之间存在明显的继承关系。所有的进程都是PID为1的init进程的后代。内核会在系统启动的最后阶段启动init进程,这个进程回去读取并且执行系统的初始化脚本(initscript)执行相关程序,完成整个系统的启动。
在Linux操作系统中,每个进程都会有父进程,每个进程都会有0到n个子进程。同一个父进程的所有进程被称为兄弟。进程描述符中,包含了指向父进程的指针,还包含了一个children子进程链表(init进程的进程描述符是静态分配的)。所以通过简单的遍历就可访问到系统中的所有进程。在代码中特意提供了for_each_process(task)宏来进行对整个进程队列(或称任务队列)的访问能力。

一、进程与线程

2.3.1 删除进程描述符

进程在执行完do_exit()函数调用之后,会处于EXIT_ZOMBIE退出状态,其所占有的内存就是内核栈thread_info结构task_struct结构体。处于这个状态的进程唯一目的就是向父进程提供信息。父进程检索到信息或者通知内核那是无关的信息后,由进程所持有的剩余的内存释放。

调用do_exit()之后,虽然线程已经僵死不再运行,但是系统还保留了它的进程描述符。这样做可以使系统能够在子进程终结后仍获取其信息。所以进程的终结清理操作可以和进程描述符的删除操作分开运行。
在删除进程描述符的时候,会调用release_task(),完成以下操作:
(1)调用__exit_signal(),由次函数调用_unhash_process(),后者又调用detach_pid()pidhash上删除该进程,同时从任列表中删除该进程
2)__exit_signal()释放目前僵死进程所使用的所有剩余资源,并进行最终的统计和记录。
3)如果这个进程是进程组最后一个进程,并且领头进程已经死掉,那么release_task()通知僵死的领头进程的父进程
4)调用put_task_struct()释放进程内核栈thread_info结构所占用的页,释放task_struct所占的slab高速缓存

若父进程在子进程之前退出,则首先会为子进程在当前进程组内宣召一个进程作为父亲,若不行,就让init进程作为父进程。

二、进程描述符及任务结构

可以使用 set_task_state(task,state) 函数来设置当前进程状态:

    内核把进程的列表存放在任务队列中,任务队列是一个双向循环链表如图1所示。链表中每一项都是类型为 task_struct 的结构体,被称为 进程描述符,该结构定义在 <linux/sched.h>文件中。进程描述符中包含一个具体进程的所有信息。进程描述符中包含的数据能完整地描述一个正在执行的程序:它打开的文件、进程的地址空间、挂起的信号、进程的状态以及其他信息。 

 

     调用 do_exit() 来终结进程。当一个进程被终结时,内核必须释放它所占有的资源,并告知其父进程。

 

四、进程终结

2)进程状态    

    linux使用 fork() 和 exec() 函数来创建进程。首先,使用 fork()函数拷贝当前进程创建一个子进程,这个子进程与父进程之间的区别仅在于 PID、PPID 以及某些资源统计量不同;然后调用 exec() 函数,把当前进程映像替换成新的进程文件,得到一个新程序。

struct thread_info{
    struct task_struct     *task;
    struct exec_domain     *exec_domain;
    _u32                   flags;
    _u32                   status;
    _u32                   cpu;
    int                    preempt_count;
    mm_segment_t           addr_limit;
    struct restart_block   restart_block;
    void                   *sysenter_return;
    int                    uaccess_err;
};

 

    进程描述符中的 state 域描述了进程的当前状态。系统中进程的状态大致有以下这几种:

TASK_RUNNING(运行) 表示进程正在执行,或者在运行队列中等待执行;
TASK_INTERRUPTIBLE(可中断)

表示进程正在睡眠(被阻塞),等待某些条件的达成。一旦这些条件达成,内核就会把进程状态设置为运行,处于此状态的进程也会因为接收到信号而提前被唤醒并随时准备投入运行;

 TASK_UNINTERRUPTIBLE(不可中断) 除了就算接收到信号也不会被唤醒或者准备投入运行外,这个状态与可中断状态相同。这个状态通常在进程必须等待时不受干扰或者等待事件很快就会发生时出现;
__TASK_TRACED 被其他进程跟踪的进程;
 __TASK_STOPPED(停止)

进程停止执行,进程没有投入运行也不能投入运行。通常,这种状态发生在接收到 SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号的时候。此外,在调试期间接收到任何信号,都会使进程进入这种状态。

EXIT_ZOMBIE(僵死状态)

进程已经退出,但是进程本身所占的内存还没有被释放,如进程描述符等结构还保留着,以便父进程能够获得其停止运行的信息。当父进程获得需要的信息或者通知内核剩余的信息没用时,进程所占有的剩余的资源将被释放

EXIT_DEAD(死亡状态) 进程所占用的所有资源完全被释放

本文由澳门威斯尼人平台登录发布于 操作系统,转载请注明出处:Linux内核学习之进程和线程初探,进程管理

相关阅读