普通线程和内核线程

Linux内核线程kernel thread详解

内核线程


为什么需要内核线程


Linux内核可以看作一个服务进程(管理软硬件资源,响应用户进程的种种合理以及不合理的请求)。

内核需要多个执行流并行,为了防止可能的阻塞,支持多线程是必要的。

内核线程就是内核的分身,一个分身可以处理一件特定事情。内核线程的调度由内核负责,一个内核线程处于阻塞状态时不影响其他的内核线程,因为其是调度的基本单位。

这与用户线程是不一样的。因为内核线程只运行在内核态

因此,它只能使用大于PAGE_OFFSET(传统的x86_32上是3G)的地址空间。

内核线程概述


内核线程是直接由内核本身启动的进程。内核线程实际上是将内核函数委托给独立的进程,它与内核中的其他进程”并行”执行。内核线程经常被称之为内核守护进程

他们执行下列任务

  • 周期性地将修改的内存页与页来源块设备同步

  • 如果内存页很少使用,则写入交换区

  • 管理延时动作, 如2号进程接手内核进程的创建

  • 实现文件系统的事务日志

内核线程主要有两种类型

  1. 线程启动后一直等待,直至内核请求线程执行某一特定操作。

  2. 线程启动后按周期性间隔运行,检测特定资源的使用,在用量超出或低于预置的限制时采取行动。

内核线程由内核自身生成,其特点在于

  1. 它们在CPU的管态执行,而不是用户态。

  2. 它们只可以访问虚拟地址空间的内核部分(高于TASK_SIZE的所有地址),但不能访问用户空间

内核线程的进程描述符task_struct


task_struct进程描述符中包含两个跟进程地址空间相关的字段mm, active_mm,

struct task_struct
{
    // ...
    struct mm_struct *mm;
    struct mm_struct *avtive_mm;
    //...
};

大多数计算机上系统的全部虚拟地址空间分为两个部分: 供用户态程序访问的虚拟地址空间和供内核访问的内核空间。每当内核执行上下文切换时, 虚拟地址空间的用户层部分都会切换, 以便当前运行的进程匹配, 而内核空间不会放生切换。

对于普通用户进程来说,mm指向虚拟地址空间的用户空间部分,而对于内核线程,mm为NULL。

这位优化提供了一些余地, 可遵循所谓的惰性TLB处理(lazy TLB handing)。active_mm主要用于优化,由于内核线程不与任何特定的用户层进程相关,内核并不需要倒换虚拟地址空间的用户层部分,保留旧设置即可。由于内核线程之前可能是任何用户层进程在执行,故用户空间部分的内容本质上是随机的,内核线程决不能修改其内容,故将mm设置为NULL,同时如果切换出去的是用户进程,内核将原来进程的mm存放在新内核线程的active_mm中,因为某些时候内核必须知道用户空间当前包含了什么。

为什么没有mm指针的进程称为惰性TLB进程?

假如内核线程之后运行的进程与之前是同一个, 在这种情况下, 内核并不需要修改用户空间地址表。地址转换后备缓冲器(即TLB)中的信息仍然有效。只有在内核线程之后, 执行的进程是与此前不同的用户层进程时, 才需要切换(并对应清除TLB数据)。

内核线程和普通的进程间的区别在于内核线程没有独立的地址空间,mm指针被设置为NULL;它只在 内核空间运行,从来不切换到用户空间去;并且和普通进程一样,可以被调度,也可以被抢占。

内核线程的创建


创建内核线程接口的演变


内核线程可以通过两种方式实现:

  • 古老的接口 kernel_create和daemonize

    将一个函数传递给kernel_thread创建并初始化一个task,该函数接下来负责帮助内核调用daemonize已转换为内核守护进程,daemonize随后完成一些列操作, 如该函数释放其父进程的所有资源,不然这些资源会一直锁定直到线程结束。阻塞信号的接收, 将init用作守护进程的父进程

  • 更加现在的方法kthead_create和kthread_run

    创建内核更常用的方法是辅助函数kthread_create,该函数创建一个新的内核线程。最初线程是停止的,需要使用wake_up_process启动它。

    使用kthread_run,与kthread_create不同的是,其创建新线程后立即唤醒它,其本质就是先用kthread_create创建一个内核线程,然后通过wake_up_process唤醒它

2号进程kthreadd的诞生


早期的kernel_create和daemonize接口

在早期的内核中, 提供了kernel_create和daemonize接口, 但是这种机制操作复杂而且将所有的任务交给内核去完成。

但是这种机制低效而且繁琐, 将所有的操作塞给内核, 我们创建内核线程的初衷不本来就是为了内核分担工作, 减少内核的开销的么

Workqueue机制

因此在linux-2.6以后, 提供了更加方便的接口kthead_create和kthread_run, 同时将内核线程的创建操作延后, 交给一个工作队列workqueue, 参见http://lxr.linux.no/linux+v2.6.13/kernel/kthread.c#L21

Linux中的workqueue机制就是为了简化内核线程的创建。通过kthread_create并不真正创建内核线程, 而是将创建工作create work插入到工作队列helper_wq中, 随后调用workqueue的接口就能创建内核线程。并且可以根据当前系统CPU的个数创建线程的数量,使得线程处理的事务能够并行化。workqueue是内核中实现简单而有效的机制,他显然简化了内核daemon的创建,方便了用户的编程.

工作队列(workqueue)是另外一种将工作推后执行的形式.工作队列可以把工作推后,交由一个内核线程去执行,也就是说,这个下半部分可以在进程上下文中执行。最重要的就是工作队列允许被重新调度甚至是睡眠。

具体的信息, 请参见

Linux workqueue工作原理

2号进程kthreadd

但是这种方法依然看起来不够优美, 我们何不把这种创建内核线程的工作交给一个特殊的内核线程来做呢?

于是linux-2.6.22引入了kthreadd进程, 并随后演变为2号进程, 它在系统初始化时同1号进程一起被创建(当然肯定是通过kernel_thread), 参见rest_init函数, 并随后演变为创建内核线程的真正建造师, 参见kthreaddkthreadd函数, 它会循环的是查询工作链表static LIST_HEAD(kthread_create_list);中是否有需要被创建的内核线程, 而我们的通过kthread_create执行的操作, 只是在内核线程任务队列kthread_create_list中增加了一个create任务, 然后会唤醒kthreadd进程来执行真正的创建操作

内核线程会出现在系统进程列表中, 但是在ps的输出中进程名command由方括号包围, 以便与普通进程区分。

如下图所示, 我们可以看到系统中, 所有内核线程都用[]标识, 而且这些进程父进程id均是2, 而2号进程kthreadd的父进程是0号进程

使用ps -eo pid,ppid,command

ps查看内核线程

kernel_thread


kernel_thread是最基础的创建内核线程的接口, 它通过将一个函数直接传递给内核来创建一个进程, 创建的进程运行在内核空间, 并且与其他进程线程共享内核虚拟地址空间

kernel_thread的实现经历过很多变革
早期的kernel_thread执行更底层的操作, 直接创建了task_struct并进行初始化,

引入了kthread_create和kthreadd 2号进程后, kernel_thread的实现也由统一的_do_fork(或者早期的do_fork)托管实现

早期实现

早期的内核中, kernel_thread并不是使用统一的do_fork或者_do_fork这一封装好的接口实现的, 而是使用更底层的细节

参见

http://lxr.free-electrons.com/source/kernel/fork.c?v=2.4.37#L613

我们可以看到它内部调用了更加底层的arch_kernel_thread创建了一个线程

arch_kernel_thread

其具体实现请参见

http://lxr.free-electrons.com/ident?v=2.4.37;i=arch_kernel_thread

但是这种方式创建的线程并不适合运行,因此内核提供了daemonize函数, 其声明在include/linux/sched.h中

//  http://lxr.free-electrons.com/source/include/linux/sched.h?v=2.4.37#L800
extern void daemonize(void);

定义在kernel/sched.c

http://lxr.free-electrons.com/source/kernel/sched.c?v=2.4.37#L1326

主要执行如下操作

  1. 该函数释放其父进程的所有资源,不然这些资源会一直锁定直到线程结束。

  2. 阻塞信号的接收

  3. 将init用作守护进程的父进程

我们可以看到早期内核的很多地方使用了这个接口, 比如

可以参见

http://lxr.free-electrons.com/ident?v=2.4.37;i=daemonize

我们将了这么多kernel_thread, 但是我们并不提倡我们使用它, 因为这个是底层的创建内核线程的操作接口, 使用kernel_thread在内核中执行大量的操作, 虽然创建的代价已经很小了, 但是对于追求性能的linux内核来说还不能忍受

因此我们只能说kernel_thread是一个古老的接口, 内核中的有些地方仍然在使用该方法, 将一个函数直接传递给内核来创建内核线程

新版本的实现

于是linux-3.x下之后, 有了更好的实现, 那就是


延后内核的创建工作, 将内核线程的创建工作交给一个内核线程来做, 即kthreadd 2号进程

但是在kthreadd还没创建之前, 我们只能通过kernel_thread这种方式去创建,

同时kernel_thread的实现也改为由_do_fork(早期内核中是do_fork)来实现, 参见kernel/fork.c

pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{
    return _do_fork(flags|CLONE_VM|CLONE_UNTRACED, (unsigned long)fn,
            (unsigned long)arg, NULL, NULL, 0);
}

kthread_create


struct task_struct *kthread_create_on_node(int (*threadfn)(void *data),
                                           void *data,
                                          int node,
                                          const char namefmt[], ...);

#define kthread_create(threadfn, data, namefmt, arg...) \
       kthread_create_on_node(threadfn, data, NUMA_NO_NODE, namefmt, ##arg)

创建内核更常用的方法是辅助函数kthread_create,该函数创建一个新的内核线程。最初线程是停止的,需要使用wake_up_process启动它。

kthread_run


/**
 * kthread_run - create and wake a thread.
 * @threadfn: the function to run until signal_pending(current).
 * @data: data ptr for @threadfn.
 * @namefmt: printf-style name for the thread.
 *
 * Description: Convenient wrapper for kthread_create() followed by
 * wake_up_process().  Returns the kthread or ERR_PTR(-ENOMEM).
 */
#define kthread_run(threadfn, data, namefmt, ...)                          \
({                                                                         \
    struct task_struct *__k                                            \
            = kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \
    if (!IS_ERR(__k))                                                  \
            wake_up_process(__k);                                      \
    __k;                                                               \
})

使用kthread_run,与kthread_create不同的是,其创建新线程后立即唤醒它,其本质就是先用kthread_create创建一个内核线程,然后通过wake_up_process唤醒它

内核线程的退出


线程一旦启动起来后,会一直运行,除非该线程主动调用do_exit函数,或者其他的进程调用kthread_stop函数,结束线程的运行。

    int kthread_stop(struct task_struct *thread);

kthread_stop() 通过发送信号给线程。

如果线程函数正在处理一个非常重要的任务,它不会被中断的。当然如果线程函数永远不返回并且不检查信号,它将永远都不会停止。

在执行kthread_stop的时候,目标线程必须没有退出,否则会Oops。原因很容易理解,当目标线程退出的时候,其对应的task结构也变得无效,kthread_stop引用该无效task结构就会出错。

为了避免这种情况,需要确保线程没有退出,其方法如代码中所示:

thread_func()
{
    // do your work here
    // wait to exit
    while(!thread_could_stop())
    {
           wait();
    }
}

exit_code()
{
     kthread_stop(_task);   //发信号给task,通知其可以退出了
}

这种退出机制很温和,一切尽在thread_func()的掌控之中,线程在退出时可以从容地释放资源,而不是莫名其妙地被人“暗杀”。

 

 

 

 

Linux 内核线程及普通进程总结

1 Linux 中的进程与线程

对于 Linux 来讲,所有的线程都当作进程来实现,因为没有单独为线程定义特定的调度算法,也没有单独为线程定义特定的数据结构(所有的线程或进程的核心数据结构都是 task_struct)。

对于一个进程,相当于是它含有一个线程,就是它自身。对于多线程来说,原本的进程称为主线程,它们在一起组成一个线程组。

进程拥有自己的地址空间,所以每个进程都有自己的页表。而线程却没有,只能和其它线程共享某一个地址空间和同一份页表。

这个区别的 根本原因 是,在进程/线程创建时,因是否拷贝当前进程的地址空间还是共享当前进程的地址空间,而使得指定的参数不同而导致的。

具体地说,进程和线程的创建都是执行 clone 系统调用进行的。而 clone 系统调用会执行 do_fork 内核函数,而它则又会调用 copy_process 内核函数来完成。主要包括如下操作:

  • 在调用 copy_process 的过程中,会创建并拷贝当前进程的 task_stuct,同时还会创建属于子进程的 thread_info 结构以及内核栈。
  • 此后,会为创建好的 task_stuct 指定一个新的 pid(在 task_struct 结构体中)。
  • 然后根据传递给 clone 的参数标志,来选择拷贝还是共享打开的文件,文件系统信息,信号处理函数,进程地址空间等。这就是进程和线程不一样地方的本质所在

2 三个数据结构

每个进程或线程都有三个数据结构,分别是 struct thread_info, struct task_struct内核栈

注意,虽然线程与主线程共享地址空间,但是线程也是有自己独立的内核栈的。

thread_info 对象中存放的进程/线程的基本信息,它和这个进程/线程的内核栈存放在内核空间里的一段 2 倍页长的空间中。其中 thread_info 结构存放在低地址段的末尾,其余空间用作内核栈。内核使用 伙伴系统 为每个进程/线程分配这块空间。

thread_info 结构体中有一个 struct task_struct *task,task 指向的就是这个进程或线程相关的 task_struct 对象(也在内核空间中),这个对象叫做进程描述符(叫做任务描述符更为贴切,因为每个线程也都有自己的 task_struct)。内核使用 slab 分配器为每个进程/线程分配这块空间。

如下图所示:

1.png-94kB

3 task_struct 结构体

每个进程或线程都有只属于自己的 task_struct 对象,是它们各自最为核心的数据结构。

3.1 task_struct 结构体中的主要元素

  • struct thread_info *thread_info。thread_info 指向该进程/线程的基本信息。
  • struct mm_struct *mm。mm_struct 对象用来管理该进程/线程的页表以及虚拟内存区。
  • struct mm_struct *active_mm。主要用于内核线程访问主内核页全局目录。
  • struct fs_struct *fs。fs_struct 是关于文件系统的对象。
  • struct files_struct *files。files_struct 是关于打开的文件的对象。
  • struct signal_struct *signal。signal_struct 是关于信号的对象。

3.2 task_struct 结构体中的三个 ID 与一个指针

  • pid
    每个 task_struct 都会有一个不同的 ID,就是这个 PID。
  • tid
    线程 ID,用来标识每个线程的。
  • tgid
    线程组领头线程的 PID,事实上就是主线程的 PID。
    当创建一个子进程时,它的 tgid 与 pid 相等;
    当创建一个线程时,它的 tgid 等于主线程的 pid。

    getpid() 函数事实上返回的是当前进程或线程的 tgid。

  • pgid
    进程组领头进程的 PID。
  • sid
    会话领头进程的 PID。
  • group_leader
    是一个 task_struct 类型的指针,指向的是进程组的组长对应的 task_struct 对象。

4 虚拟内存地址空间

4.1 内存管理

内存是由内核来管理的。

内存被分为 n 个页框,然后进一步组织为多个区。而装入页框中的内容称为页。

当内核函数申请内存时,内核总是立即满足(因为内核完全信任它们,所以优先级最高)。在分配适当内存空间后,将其映射到内核地址空间中(3-4GB 中的某部分空间),然后将地址映射写入页表。

申请内存空间的内核函数有 vmalloc, kmalloc, alloc_pages, __get_free_pages 等。

4.2 内核常驻内存

就是说,内核地址空间(3-4GB)中的页面所映射的页框始终在物理内存中存在,不会被换出。即使是 vmalloc 动态申请的页面也会一直在物理内存中,直至通过相关内核函数释放掉。

其原因在于,一方面内核文件不是太大,完全可以一次性装入物理内存;另一方面在于即使是动态申请内存空间,也能立即得到满足。

因此,处于内核态的普通进程或内核线程(后面会提到)不会因为页面没有在内存中而产生缺页异常(不过处于内核态的普通进程会因为页表项没有同步的原因而产生缺页异常)。

4.3 为什么要有虚拟地址空间

普通进程在申请内存空间时会被内核认为是不紧要的,优先级较低。因而总是延迟处理,在之后的某个时候才会真正为其分配物理内存空间。

比如,普通进程中的 malloc 函数在申请物理内存空间时,内核不会直接为其分配页框。

另一方面,普通进程对应的可执行程序文件较大,不能够立即装入内存,而是采取运行时按需装入。

要实现这种延迟分配策略,就需要引入一种新的地址空间,即 虚拟地址空间。可执行文件在装入时或者进程在执行 malloc 时,内核只会为其分配适当大小的虚拟地址空间。

虚拟地址空间并不单纯地指线性地址空间。准确地说,指的是页面不能因为立即装入物理内存而采取折衷处理后拥有的线性地址空间。
因此,虽然普通进程的虚拟地址空间为 4GB,但是从内核的角度来说,内核地址空间(也是线性空间)不能称为虚拟地址空间,内核线程不拥有也不需要虚拟地址空间
因此,虚拟地址空间只针对普通进程。

当然,这样的话就会产生所要访问的页面不在物理内存中而发生缺页异常。

4.4 虚拟地址空间的划分

每一个普通进程都拥有 4GB 的虚拟地址空间(对于 32 位的 CPU 来说,即 2^32 B)。

主要分为两部分,一部分是用户空间(0-3GB),一部分是内核空间(3-4GB)。每个普通进程都有自己的用户空间,但是内核空间被所有普通进程所共享。

如下图所示:

2.png-41.3kB

之所以能够使用 3-4GB 的虚拟地址空间(对于普通进程来说),是因为每个进程的页全局目录(后面会提到)中的后面部分存放的是内核页全局目录的所有表项。当通过系统调用或者发生异常而陷入内核时,不会切换进程的页表。此时,处于内核态的普通进程将会直接使用进程页表中前面的页表项即可。这也是为什么在执行系统调用或者处理异常时没有发生进程的上下文切换的真实原因
同样,正因为每个进程的也全局目录中的后面部分存放的是内核页全局目录中的所有表项,所以所有普通进程共享内核空间。

另外,

  • 用户态下的普通进程只能访问 0-3GB 的用户空间;
  • 内核态下的普通进程既能访问 0-3GB 的用户空间,也能访问 3-4GB 的内核空间(内核态下的普通进程有时也会需要访问用户空间)。

4.5 普通线程的用户堆栈与寄存器

对于多线程环境,虽然所有线程都共享同一片虚拟地址空间,但是每个线程都有自己的用户栈空间和寄存器,而用户堆仍然是所有线程共享的

栈空间的使用是有明确限制的,栈中相邻的任意两条数据在地址上都是连续的。试想,假设多个普通线程函数都在执行递归操作。如果多个线程共有用户栈空间,由于线程是异步执行的,那么某个线程从栈中取出数据时,这条数据就很有可能是其它线程之前压入的,这就导致了冲突。所以,每个线程都应该有自己的用户栈空间。

寄存器也是如此,如果共用寄存器,很可能出现使用混乱的现象。

而堆空间的使用则并没有这样明确的限制,某个线程在申请堆空间时,内核只要从堆空间中分配一块大小合适的空间给线程就行了。所以,多个线程同时执行时不会出现向栈那样产生冲突的情况,因而线程组中的所有线程共享用户堆。

那么在创建线程时,内核是怎样为每个线程分配栈空间的呢?

由之前所讲解可知,进程/线程的创建主要是由 clone 系统调用完成的。而 clone 系统调用的参数中有一个 void *child_stack,它就是用来指向所创建的进程/线程的堆栈指针。

而在该进程/线程在用户态下是通过调用 pthread_create 库函数而陷入内核的。对于 pthread_create 函数,它则会调用一个名为 pthread_allocate_stack 的函数,专门用来为所创建的线程分配的栈空间(通过 mmap 系统调用)。然后再将这个栈空间的地址传递给 clone 系统调用。这也是为什么线程组中的每个线程都有自己的栈空间

4.6 普通进程的页表

有两种页表,一种是内核页表(会在后面说明),另一种是进程页表。

普通进程使用的则是进程页表,而且每个普通进程都有自己的进程页表。如果是多线程,则这些线程共享的是主线程的进程页表。

4.6.1 四级页表

现在的 Linux 内核中采用四级页表,分别为:

  • 页全局目录 (Page Global Directory, pgd);
  • 页上级目录 (Page Upper Directory, pud);
  • 页中间目录 (Page Middle Directory, pmd);
  • 页表 (Page Table, pt)。

task_struct 中的 mm_struct 对象用于管理该进程(或者线程共享的)页表。准确地说,mm_struct 中的 pgd 指针指向着该进程的页全局目录。

4.6.2 普通进程的页全局目录

普通进程的页全局目录中,第一部分表项映射的线性地址为 0-3GB 部分,剩余部分存放的是主内核页全局目录(后面会提到)中的所有表项。

5 内核线程

内核线程是一种只运行在内核地址空间的线程。所有的内核线程共享内核地址空间(对于 32 位系统来说,就是 3-4GB 的虚拟地址空间),所以也共享同一份内核页表。这也是为什么叫内核线程,而不叫内核进程的原因

由于内核线程只运行在内核地址空间中,只会访问 3-4GB 的内核地址空间,不存在虚拟地址空间,因此每个内核线程的 task_struct 对象中的 mm 为 NULL。

普通线程虽然也是同主线程共享地址空间,但是它的 task_struct 对象中的 mm 不为空,指向的是主线程的 mm_struct 对象。

普通进程与内核线程有如下区别:

  • 内核线程只运行在内核态,而普通进程既可以运行在内核态,也可以运行在用户态;
  • 内核线程只使用 3-4GB (假设为 32 位系统) 的内核地址空间(共享的),但普通进程由于既可以运行在用户态,又可以运行在内核态,因此可以使用 4GB 的虚拟地址空间。

系统在正式启动内核时,会执行 start_kernel 函数。在这个函数中,会自动创建一个进程,名为 init_task。其 PID 为 0,运行在内核态中。然后开始执行一系列初始化。

5.1 init 内核线程

init_task 在执行 rest_init 函数时,会执行 kernel_thread 创建 init 内核线程。它的 PID 为 1,用来完成内核空间初始化。

在内核空间完成初始化后,会调用 exceve 执行 init 可执行程序 (/sbin/init)。之后,init 内核线程变成了一个普通的进程,运行在用户空间中。

init 内核线程没有地址空间,且它的 task_struct 对象中的 mm 为 NULL。因此,执行 exceve 会使这个 mm 指向一个 mm_struct,而不会影响到 init_task 进程的地址空间。
也正因为此,init 在转变为进程后,其 PID 没变,仍为 1。

创建完 init 内核线程后,init_task 进程演变为 idle 进程(PID 仍为 0)。

之后,init 进程再根据再启动其它系统进程 (/etc/init.d 目录下的各个可执行文件)。

5.2 kthreadd 内核线程

init_task 进程演变为 idle 进程后,idle 进程会执行 kernel_thread 来创建 kthreadd 内核线程(仍然在 rest_init 函数中)。它的 PID 为 2,用来创建并管理其它内核线程(用 kthread_create, kthread_run, kthread_stop 等内核函数)。

系统中有很多内核守护进程 (线程),可以通过:
ps -efj
进行查看,其中带有 [] 号的就属于内核守护进程。它们的祖先都是这个 kthreadd 内核线程。

5.3 主内核页全局目录

内核维持着一组自己使用的页表,也即主内核页全局目录。当内核在初始化完成后,其存放在 swapper_pg_dir 中,而且所有的普通进程和内核线程就不再使用它了。

5.4 内核线程如何访问页表

5.4.1 active_mm

对于内核线程,虽然它的 task_struct 中的 mm 为 NULL,但是它仍然需要访问内核空间,因此需要知道关于内核空间映射到物理内存的页表。然而不再使用 swapper_pg_dir,因此只能另外想法解决。

由于所有的普通进程的页全局目录中的后面部分为主内核页全局目录,因此内核线程只需要使用某个普通进程的页全局目录就可以了。

在 Linux 中,task_struct 中还有一个很重要的元素为 active_mm,它主要就是用于内核线程访问主内核页全局目录。

  • 对于普通进程来说,task_struct 中的 mm 和 active_mm 指向的是同一片区域;
  • 然而对内核线程来说,task_struct 中的 mm 为 NULL,active_mm 指向的是前一个普通进程的 mm_struct 对象。

5.4.2 mm_users 和 mm_count

但是这样还是不行,因为如果因为前一个普通进程退出了而导致它的 mm_struct 对象也被释放了,则内核线程就访问不到了。

为此,mm_struct 对象维护了一个计数器 mm_count,专门用来对引用这个 mm_struct 对象的自身及内核线程进行计数。初始时为 1,表示普通进程本身引用了它自己的 mm_struct 对象。只有当这个引用计数为 0 时,才会真正释放这个 mm_struct 对象。

另外,mm_struct 中还定义了一个 mm_users 计数器,它主要是用来对共享地址空间的线程计数。事实上,就是这个主线程所在线程组中线程的总个数。初始时为 1。

注意,两者在实质上都是针对引用 mm_struct 对象而设置的计数器。
不同的是,mm_count 是专门针对自身及内核线程或引用 mm_struct 而进行计数;而 mm_users 是专门针对该普通线程所在线程组的所有普通线程而进行计数。
另外,只有当 mm_count 为 0 时,才会释放 mm_struct 对象,并不会因为 mm_users 为 0 就进行释放。


Reference

posted on 2018-03-08 10:28  AlanTu  阅读(13287)  评论(0编辑  收藏  举报

导航