【生意多】-免费发布分类信息
当前位置: 首页 » 新闻 » 教程 » 操作系统 » 正文

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

放大字体  缩小字体 发布日期:2022-05-01 04:39:29    浏览次数:12
导读

  对于 Linux 来讲,所有的线程都当作进程来实现,因为没有单独为线程定义特定的调度算法,也没有单独为线程定义特定的数据结

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

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

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

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

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

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

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

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

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

  tgid :线程组领头线程的 PID,事实上就是主线程的 PID。当创建一个子进程时,它的 tgid 与 pid 相等;当创建一个线程时,它的 tgid 等于主线程的 pid。getpid 函数事实上返回的是当前进程或线程的 tgid。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  进行查看,其中带有 [] 号的就属于内核守护进程。它们的祖先都是这个 kthreadd 内核线程。

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

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

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

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

  但是这样还是不行,因为如果因为前一个普通进程退出了而导致它的 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 就进行释放。

 
关键词: linux系统电脑
(文/小编)
打赏
免责声明
• 
本文为小编原创作品,作者: 小编。欢迎转载,转载请注明原文出处:http://www.31duo.com/news/show-3411285.html 。本文仅代表作者个人观点,本站未对其内容进行核实,请读者仅做参考,如若文中涉及有违公德、触犯法律的内容,一经发现,立即删除,作者需自行承担相应责任。涉及到版权或其他问题,请及时联系我们。
 

(c)2016-2019 31DUO.COM All Rights Reserved浙ICP备19001410号-4

浙ICP备19001410号-4