Linux内核线程、轻量级进程和用户进程以及其创建方式
本文声明
| 日期 | 内核版本 | 架构 | 作者 | GitHub | CSDN |
|---|---|---|---|---|---|
| 2016-05-12 | Linux-4.5 | X86 & arm | gatieme | LinuxDeviceDrivers | Linux进程管理与调度-之-进程的创建 |
本章链接
| 链接地址 | 上一节 | 本章目录 | 下一节 |
|---|---|---|---|
| CSDN | 已是第一篇 | 无 | Linux下0号进程的前世(init_task进程)今生(idle进程) |
| GitHub | 已是第一篇 | 进程的创建 | Linux下0号进程的前世(init_task进程)今生(idle进程) |
章节链接
| 链接地址 | 上一章 | 总目录 | 下一章 |
|---|---|---|---|
| CSDN | 已是第一章 | 目录 | |
| GitHub | 已是第一章 | 目录 | Linux下0号进程的前世(init_task进程)今生(idle进程) |
#Linux进程类别
虽然我们在区分Linux进程, 但是我还是想说Linux下只有一种类型的进程,那就是task_struct, 当然我也想说Linux下其实本质上没有线程的概念, 线程其实上是
##Linux下进程和线程的区别
而由于其运行空间的不同,从而演变出内核线程
##内核线程
内核线程只运行在内核态,不受用户态上下文的拖累。 处理器竞争:可以在全系统范围内竞争处理器资源; 使用资源:唯一使用的资源是内核栈和上下文切换时保持寄存器的空间 调度:调度的开销可能和进程自身差不多昂贵 同步效率:资源的同步和数据共享比整个进程的数据同步和共享要低一些。
##轻量级进程
轻量级进程(LWP)是建立在内核之上并由内核支持的用户线程,它是内核线程的高度抽象,每一个轻量级进程都与一个特定的内核线程关联。内核线程只能由内核管理并像普通进程一样被调度。 轻量级进程由clone()系统调用创建,参数是CLONE_VM,即与父进程是共享进程地址空间和系统资源。 与普通进程区别:LWP只有一个最小的执行上下文和调度程序所需的统计信息。 处理器竞争:因与特定内核线程关联,因此可以在全系统范围内竞争处理器资源 使用资源:与父进程共享进程地址空间 调度:像普通进程一样调度 用户线程 用户线程是完全建立在用户空间的线程库,用户线程的创建、调度、同步和销毁全又库函数在用户空间完成,不需要内核的帮助。因此这种线程是极其低消耗和高效的。 处理器竞争:单纯的用户线程是建立在用户空间,其对内核是透明的,因此其所属进程单独参与处理器的竞争,而进程的所有线程参与竞争该进程的资源。 使用资源:与所属进程共享进程地址空间和系统资源。 调度:由在用户空间实现的线程库,在所属进程内进行调度
#Linux下进程的创建流程
##进程的复制fork和加载execve
我们在Linux下进行进行编程,往往都是通过fork出来一个新的程序,fork从化字面意义上理解就是说"分叉", 这其实就意味着我们的fork进程并不是真正从无到有被创建出来的。
一个进程,包括代码、数据和分配给进程的资源,它其实是从现有的进程(父进程)复制出的一个副本(子进程),fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,然后如果我们通过execve为子进程加载新的应用程序后,那么新的进程将开始执行新的应用
简单来说,新的进程是通过fork和execve创建的,首先通过fork从父进程分叉出一个基本一致的副本,然后通过execve来加载新的应用程序镜像
- fork生成当前进程的的一个相同副本,该副本成为子进程
原进程(父进程)的所有资源都以适当的方法复制给新的进程(子进程)。因此该系统调用之后,原来的进程就有了两个独立的实例,这两个实例的联系包括:同一组打开文件, 同样的工作目录, 进程虚拟空间(内存)中同样的数据(当然两个进程各有一份副本, 也就是说他们的虚拟地址相同, 但是所对应的物理地址不同)等等。
- execve从一个可执行的二进制程序镜像加载应用程序, 来代替当前运行的进程
换句话说, 加载了一个新的应用程序。因此execv并不是创建新进程
所以我们在linux要创建一个应用程序的时候,其实执行的操作就是
-
首先使用fork复制一个旧的进程
-
然后调用execve在为新的进程加载一个新的应用程序
#写时复制技术
有人认为这样大批量的复制会导致执行效率过低。其实在复制过程中,linux采用了写时复制的策略。
写入时复制(Copy-on-write)是一个被使用在程式设计领域的最佳化策略。其基础的观念是,如果有多个呼叫者(callers)同时要求相同资源,他们会共同取得相同的指标指向相同的资源,直到某个呼叫者(caller)尝试修改资源时,系统才会真正复制一个副本(private copy)给该呼叫者,以避免被修改的资源被直接察觉到,这过程对其他的呼叫只都是通透的(transparently)。此作法主要的优点是如果呼叫者并没有修改该资源,就不会有副本(private copy)被建立。
第一代Unix系统实现了一种傻瓜式的进程创建:当发出fork()系统调用时,内核原样复制父进程的整个地址空间并把复制的那一份分配给子进程。这种行为是非常耗时的,这种创建地址空间的方法涉及许多内存访问,消耗许多CPU周期,并且完全破坏了高速缓存中的内容。在大多数情况下,这样做常常是毫无意义的,因为许多子进程通过装入一个新的程序开始它们的执行,这样就完全丢弃了所继承的地址空间。
现在的Linux内核采用一种更为有效的方法,称之为写时复制(Copy On Write,COW)。这种思想相当简单:父进程和子进程共享页帧而不是复制页帧。然而,只要页帧被共享,它们就不能被修改,即页帧被保护。无论父进程还是子进程何时试图写一个共享的页帧,就产生一个异常,这时内核就把这个页复制到一个新的页帧中并标记为可写。原来的页帧仍然是写保护的:当其他进程试图写入时,内核检查写进程是否是这个页帧的唯一属主,如果是,就把这个页帧标记为对这个进程是可写的。
当进程A使用系统调用fork创建一个子进程B时,由于子进程B实际上是父进程A的一个拷贝,
因此会拥有与父进程相同的物理页面.为了节约内存和加快创建速度的目标,fork()函数会让子进程B以只读方式共享父进程A的物理页面.同时将父进程A对这些物理页面的访问权限也设成只读.
这样,当父进程A或子进程B任何一方对这些已共享的物理页面执行写操作时,都会产生页面出错异常(page_fault int14)中断,此时CPU会执行系统提供的异常处理函数do_wp_page()来解决这个异常.
do_wp_page()会对这块导致写入异常中断的物理页面进行取消共享操作,为写进程复制一新的物理页面,使父进程A和子进程B各自拥有一块内容相同的物理页面.最后,从异常处理函数中返回时,CPU就会重新执行刚才导致异常的写入操作指令,使进程继续执行下去.
一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值(比如PID)不同。相当于克隆了一个自己。