Linux-进程的管理与调度4(基于6.1内核)

Linux-进程的管理与调度4(基于6.1内核)---Linux下的进程类别以及其创建方式

一、Linux进程类别


内核线程与用户进程?

 一个进程由于其运行空间的不同, 从而有内核线程用户进程的区分, 内核线程运行在内核空间, 之所以称之为线程是因为它没有虚拟地址空间, 只能访问内核的代码和数据, 而用户进程则运行在用户空间, 但是可以通过中断, 系统调用等方式从用户态陷入内核态。

 用户进程运行在用户空间上, 而一些通过共享资源实现的一组进程我们称之为线程组, Linux下内核其实本质上没有线程的概念, Linux下线程其实上是与其他进程共享某些资源的进程而已。但是我们习惯上还是称他们为线程或者轻量级进程。

因此, Linux上进程分3种,内核线程(或者叫核心进程)、用户进程、用户线程, 当然如果更严谨的,你也可以认为用户进程和用户线程都是用户进程。

1.1、进程与线程


进程

进程是操作系统分配资源和进行调度的基本单位。它是一个正在执行的程序实例,每个进程都有自己的地址空间、代码、数据以及一组系统资源(如文件描述符、信号等)。进程是操作系统对资源进行管理的基本实体。

特点

  • 独立性:每个进程拥有独立的内存空间,进程间的内存是隔离的,进程间的资源不共享。进程的独立性使得一个进程的崩溃不会直接影响到其他进程。
  • 资源管理:操作系统为每个进程分配系统资源,如内存、文件描述符、网络连接等。
  • 调度单位:进程是操作系统调度的基本单位,操作系统通过调度算法(如时间片轮转、优先级调度等)来决定哪一个进程可以运行。
  • PID(进程标识符):每个进程有唯一的进程 ID(PID),它用于在操作系统中标识和管理进程。

进程的结构

一个进程通常包括以下内容:

  • 代码段(Text Segment):存放程序的执行代码。
  • 数据段(Data Segment):存放程序的全局变量和静态变量。
  • 堆(Heap):用于动态分配内存。
  • 栈(Stack):存放局部变量、函数调用信息等。
  • PCB(Process Control Block):存放进程的管理信息,如进程状态、程序计数器(PC)、调度信息、内存信息等。

线程

线程是进程内部的一个执行单元。一个进程可以包含一个或多个线程,多个线程共享同一进程的地址空间和资源,但每个线程有自己的寄存器、堆栈和程序计数器。线程是操作系统调度的最小单位,也称为轻量级进程(LWP, Light Weight Process)。

特点

  • 共享资源:同一进程中的多个线程共享进程的内存空间、文件描述符等资源,但每个线程有自己独立的栈和寄存器。
  • 独立执行:线程是独立调度的,每个线程都有自己的执行流。
  • 轻量级:线程的创建和销毁比进程更为高效,因为它们共享进程的资源,减少了内存和时间开销。
  • 线程 ID:线程有自己的标识符(如线程 ID),可以用于标识和管理线程。

线程的结构

  • 线程控制块(TCB, Thread Control Block):线程的管理信息,包含线程 ID、程序计数器、堆栈指针、寄存器、调度信息等。
  • 栈(Stack):每个线程都有自己的栈,用于存放局部变量、函数调用信息等。

进程与线程的区别

特性进程(Process)线程(Thread)
资源分配进程拥有独立的资源(内存空间、文件描述符、信号等)。线程共享进程的资源,但每个线程有自己的栈和寄存器。
内存空间每个进程有独立的地址空间,进程间内存隔离。线程共享进程的地址空间,线程间的内存是共享的。
创建开销创建和销毁进程的开销较大,需要分配独立的资源。线程创建和销毁的开销较小,因为线程共享进程的资源。
调度单位进程是操作系统的调度单位,操作系统调度进程执行。线程是最小的调度单位,操作系统调度线程执行。
通信方式进程间通信(IPC)通常需要通过管道、消息队列、共享内存等方式。线程间可以直接访问共享内存,因此线程间通信更为高效。
上下文切换开销进程的上下文切换开销较大,因为需要保存和恢复完整的进程状态。线程的上下文切换开销较小,因为线程共享进程的资源。
崩溃影响如果进程崩溃,它不会直接影响其他进程。如果一个线程崩溃,可能会导致整个进程崩溃,因为它们共享同一地址空间。
PID/线程 ID每个进程有唯一的 PID(进程 ID)。每个线程有唯一的线程 ID(TID),但它们共享同一进程的 PID。

1.2、内核线程


内核线程是操作系统内核创建并管理的线程,它与用户线程不同的是,它不需要由用户进程启动,而是由内核直接创建和调度。内核线程通常用于执行操作系统的底层任务,如设备驱动程序、文件系统操作、内存管理、网络协议处理等。

内核线程的执行环境与进程相似,但它们的资源使用有所不同。内核线程可以访问所有的内核资源,而不需要用户空间的权限检查,因此它们通常拥有比用户线程更高的权限。

内核线程的特点

1. 内核态执行

内核线程是在内核态执行的,这意味着它可以直接访问内核数据结构和资源,而不需要经过用户态和内核态之间的切换。内核线程不需要进行用户态与内核态之间的上下文切换,因此在执行时的性能较高。

2. 没有用户空间

内核线程通常没有独立的用户空间,它们直接使用内核的资源和内存。与用户线程不同,内核线程通常不涉及用户空间的程序和数据,因此无法进行用户程序的输入输出操作。

3. 调度和管理

内核线程的调度由操作系统内核负责,内核会为每个内核线程分配 CPU 时间片。内核线程调度的方式与进程和用户线程类似,但由于内核线程运行在内核态,它们不需要进行用户态与内核态之间的切换,从而减少了调度开销。

4. 共享内核资源

内核线程之间共享内核的资源和内存空间。例如,多个内核线程可以共享操作系统内核的内存池、文件系统、设备驱动等资源,而每个内核线程有自己的栈和寄存器。

5. 独立性强

每个内核线程独立于其他线程,它们通常独立执行特定的内核任务。内核线程之间的通信和同步可以通过内核提供的同步机制(如信号量、互斥锁、条件变量等)进行。

6. 多核处理器支持

在现代多核处理器上,内核线程能够有效地并行执行。内核线程通常被设计成可以在多个处理器核心上并行运行,从而更好地利用硬件资源。

7. 没有用户级线程库

内核线程不依赖于用户级线程库,它们是由操作系统内核直接创建和管理的。这意味着内核线程的创建和销毁通常比用户级线程更为高效,因为它们不需要涉及用户级的调度和管理。

内核线程与用户线程的比较

特性内核线程(Kernel Thread)用户线程(User Thread)
执行环境在内核态执行,直接操作内核资源。在用户态执行,必须通过系统调用访问内核资源。
资源管理共享内核资源,不需要用户空间。依赖于用户进程,并且受限于用户空间。
调度单位由内核进行调度。由用户级线程库进行调度,或者由内核调度(如果是内核线程)。
创建和销毁开销创建和销毁较轻量,不涉及用户态。创建和销毁较重,需要涉及用户态与内核态的上下文切换。
权限级别拥有更高的权限,能直接访问内核资源。权限较低,必须通过系统调用才能访问内核资源。
上下文切换开销上下文切换开销较低,直接在内核态执行。上下文切换开销较大,涉及用户态与内核态之间的切换。

二、linux进程的创建流程


线程机制式现代编程技术中常用的一种抽象概念。该机制提供了同一个程序内共享内存地址空间,打开文件和资源的一组线程。

2.1、进程的复制fork和加载execve


一个进程,包括代码、数据和分配给进程的资源,它其实是从现有的进程(父进程)复制出的一个副本(子进程),fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,然后如果我们通过execve为子进程加载新的应用程序后,那么新的进程将开始执行新的应用

简单来说,新的进程是通过fork和execve创建的,首先通过fork从父进程分叉出一个基本一致的副本,然后通过execve来加载新的应用程序镜像

1、fork 是一种系统调用,用于在类 Unix 操作系统(如 Linux 和 macOS)中创建一个新的进程。调用 fork 时,当前进程(父进程)会创建一个几乎完全相同的子进程(子进程)。新的子进程会继承父进程的许多属性,如文件描述符、环境变量和内存空间等,但两者拥有不同的进程ID(PID)和父进程ID(PPID)。

2、execve 是一个系统调用,用于在当前进程中执行一个新的程序,并替换当前进程的映像。它是 exec 系列函数之一,execve 是最基础的版本,通常用于将当前进程的执行代码、数据、堆栈等完全替换成指定程序的内容。执行 execve 后,当前进程的进程 ID(PID)保持不变,但其程序代码、数据等会被新程序替换,原进程的执行流结束。

2.2、写时复制技术


写时复制(Copy-On-Write,COW) 是一种延迟复制技术,广泛应用于操作系统内存管理、文件系统、虚拟内存和进程管理等领域。其核心思想是在数据被修改之前,延迟实际的复制操作,而是等到需要修改数据时才进行复制。这样能够节省内存和提高效率,特别是在多个进程需要访问相同数据时。

写时复制的基本原理

当多个进程共享同一块内存区域时,COW 技术会在不进行实际内存复制的情况下,允许多个进程同时访问这块内存。只有当某个进程尝试修改这块共享内存时,操作系统才会为该进程创建该内存的一个副本,从而避免了不必要的内存复制。

具体来说,COW 工作的流程是:

  1. 共享内存:多个进程共享同一块内存页(例如,在 fork 系统调用时),这块内存页的内容不会被复制,而是被标记为只读。

  2. 修改时复制:当某个进程尝试修改该共享内存页时,操作系统会检测到写入请求,并且会将该内存页复制一份给该进程。修改操作只会作用于复制后的新内存页。

  3. 数据一致性:其他进程仍然共享原始的内存页,不会受到影响,直到它们也进行写操作。这种方式减少了不必要的内存复制和性能损耗。

写时复制的应用场景

  1. 进程的 fork 操作: 在 Unix 系统中,fork() 系统调用用于创建一个新的子进程。子进程会复制父进程的地址空间,但实际的数据并不会在 fork() 时被复制,而是父子进程共享同一块内存区域。此时,如果父进程或子进程需要修改某块内存时,操作系统才会为其创建一个新的内存副本。通过这种方式,fork 操作非常高效,避免了不必要的内存复制。

  2. 虚拟内存中的页面共享: 在使用虚拟内存时,操作系统常常将进程的虚拟地址空间与物理内存进行映射。COW 可以在多个进程之间共享只读内存页(比如共享库的内存),直到某个进程需要修改这些页面,这时系统才会为该进程分配一份新的物理内存页面。

  3. 文件系统中的 COW: 一些文件系统(如 Btrfs 和 ZFS)也实现了写时复制的技术。例如,当用户对文件进行修改时,文件系统不会立即覆盖原文件,而是先在新的位置创建一个副本并进行修改。这种方式不仅节省了磁盘空间,还提高了操作的效率,尤其是在处理大文件时。

  4. 内存管理: 写时复制还用于内存管理中,例如分配新的内存时,操作系统可能会使用 COW 来避免不必要的数据复制。在某些情况下,如果一个进程对内存中的某些数据进行修改,系统只会为该进程创建新的副本,而不会影响其他进程。

写时复制的实现

在操作系统中,COW 的实现通常通过页表和内存保护机制来实现。以下是一些常见的实现方式:

  1. 标记内存页为只读: 当多个进程共享一块内存区域时,操作系统会将该区域的内存页标记为只读。当某个进程尝试写入该内存页时,CPU 会触发一个“写时复制”的异常(通常是页面错误或段错误)。

  2. 页面错误处理: 当写时复制的内存页被写入时,操作系统会捕获页面错误并执行以下操作:

    • 分配新的内存页。
    • 将原始内存页的内容复制到新的内存页中。
    • 更新进程的页表,将其指向新的内存页。
    • 允许进程继续写入新页。
  3. 使用 mmapMAP_PRIVATE: 在许多现代操作系统中,COW 技术也通过内存映射(mmap)机制实现。比如,当使用 mmap 映射一个文件时,如果采用 MAP_PRIVATE 标志,修改映射区域的内容时也会触发写时复制。当进程修改该内存区域时,操作系统会将修改的内容写入到新的页面,而不影响原始的文件数据。

        现在的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)不同。相当于克隆了一个自己。

三、不同操作系统线程的实现机制


3.1、Linux下线程的实现机制


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

进程的pid号的时候我们就提到了, 进程task_struct中pid存储的是内核对该进程的唯一标示, 即对进程则标示进程号, 对线程来说就是其线程号, 那么对于线程来说一个线程组所有线程与领头线程具有相同的进程号,存入tgid字段

因此。getpid()返回当前进程的进程号,返回的应该是tgid值而不是pid的值, 对于用户空间来说同组的线程拥有相同进程号即tpid, 而对于内核来说, 某种程度上来说不存在线程的概念, 那么pid就是内核唯一区分每个进程的标示。

正是linux下组管理, 写时复制等这些巧妙的实现方式:

  • linux下进程或者线程的创建开销很小

  • 既然不管是线程或者进程内核都是不加区分的,一组共享地址空间或者资源的线程可以组成一个线程组, 那么其他进程即使不共享资源也可以组成进程组, 甚至来说一组进程组也可以组成会话组, 进程组可以简化向所有组内进程发送信号的操作, 一组会话也更能适应多道程序环境。

3.2、实现机制的区别


总而言之, Linux中线程与专门线程支持系统是完全不同的

Unix System V和Sun Solaris将用户线程称作为轻量级进程(LWP-Light-weight process), 相比较重量级进程, 线程被抽象成一种耗费较少资源, 运行迅速的执行单元。

而对于linux来说, 用户线程只是一种进程间共享资源的手段, 相比较其他系统的进程来说, linux系统的进程本身已经很轻量级了。

举个例子来说, 假如我们有一个包括了四个线程的进程,在提供专门线程支持的系统中, 通常会有一个包含只想四个不同线程的指针的进程描述符。该描述符复制描述像地址空间, 打开的文件这样的共享资源。线程本身再去描述它独占的资源。相反, Linux仅仅创建了四个进程, 并分配四个普通的task_struct结构, 然后建立这四个进程时制定他们共享某些资源。

四、内核线程


Linux内核可以看作一个服务进程(管理软硬件资源,响应用户进程的种种合理以及不合理的请求)。内核需要多个执行流并行,为了防止可能的阻塞,多线程化是必要的。

内核线程就是内核的分身,一个分身可以处理一件特定事情。Linux内核使用内核线程来将内核分成几个功能模块,像kswapd、kflushd等,这在处理异步事件如异步IO时特别有用。内核线程的使用是廉价的,唯一使用的资源就是内核栈和上下文切换时保存寄存器的空间。支持多线程的内核叫做多线程内核(Multi-Threads kernel )。内核线程的调度由内核负责,一个内核线程处于阻塞状态时不影响其他的内核线程,因为其是调度的基本单位。这与用户线程是不一样的。

内核线程只运行在内核态,不受用户态上下文的拖累。

  • 处理器竞争:可以在全系统范围内竞争处理器资源;

  • 使用资源:唯一使用的资源是内核栈和上下文切换时保持寄存器的空间

  • 调度:调度的开销可能和进程自身差不多昂贵

  • 同步效率:资源的同步和数据共享比整个进程的数据同步和共享要低一些。

4.1、内核线程与普通进程的异同


相同点:

1、都具有独立的执行流: 无论是内核线程还是普通进程,它们都是操作系统中独立的执行单元,能够执行自己的代码,并与其他线程或进程并发执行。

2、都需要操作系统调度: 内核线程和普通进程都由操作系统调度,操作系统需要分配CPU时间片来执行它们。在多核系统中,操作系统还可以在不同的核心上调度它们并行执行。

3、都可以共享资源: 内核线程和普通进程在某些情况下都可以共享资源,例如内存、文件描述符等。具体是否共享资源取决于它们的关系(如父子进程之间共享部分资源,线程共享相同的地址空间等)。

4、都可以并发执行: 两者都支持并发执行,能够通过多线程或多进程的方式提高系统的执行效率,处理多个任务。

不同点:

特性内核线程(Kernel Thread)普通进程(User Process)
定义内核线程是由操作系统内核管理的线程,通常用于处理内核中的任务。普通进程是由操作系统用户空间管理的独立执行单元,通常用于应用程序。
执行上下文内核线程执行在内核态,具有更高的特权级别,直接访问硬件和内核资源。普通进程执行在用户态,受限于用户空间,不允许直接访问硬件。
资源分配内核线程在内核空间运行,不需要用户空间内存,因此没有独立的地址空间。普通进程有自己的独立地址空间,操作系统为每个进程分配独立的内存。
上下文切换内核线程的上下文切换通常较快,因为它们共享内核地址空间,且通常只涉及内核态切换。普通进程的上下文切换相对较慢,因为需要切换用户态和内核态,并切换内存地址空间。
创建和销毁创建内核线程的开销较小,销毁也较为高效。内核线程的生命周期通常较短。创建和销毁进程的开销较大,因为需要分配和释放独立的地址空间、资源等。
执行环境内核线程运行在操作系统内核态,因此可以访问内核数据结构和硬件资源。普通进程运行在用户态,无法直接访问硬件或内核数据结构,需要通过系统调用与内核交互。
资源隔离内核线程没有独立的虚拟地址空间,它们共享相同的内核空间。普通进程拥有独立的虚拟地址空间,内存隔离较好,防止一个进程影响另一个进程。
调度和管理内核线程由内核调度,通常是与进程调度一起进行的。它们可以并发运行,也可以在内核中被抢占。普通进程由操作系统内核管理调度,但通常在用户空间执行,调度开销较大。
使用场景用于执行内核任务,如设备驱动、文件系统操作、内存管理等。用于执行应用程序任务,如浏览器、文字处理软件、命令行程序等。

4.2、内核线程创建


在内核中,有两种方法可以生成内核线程,一种是使用kernel_thread()接口,另一种是用kthread_create()接口

1、kernel_thread

kernel_thread接口,使用该接口创建的线程。

include/linux/sched/task.h

extern pid_t kernel_thread(int (*fn)(void *), void *arg, const char *name,
			    unsigned long flags);

2、kthread_create

而kthread_create接口,则是标准的内核线程创建接口,只须调用该接口便可创建内核线程;默认创建的线程是存于不可运行的状态,所以需要在父进程中通过调用wake_up_process()函数来启动该线程。

include/linux/kthread.h

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

线程创建后,不会马上运行,而是需要将kthread_create() 返回的task_struct指针传给wake_up_process(),然后通过此函数运行线程。

3、kthread_run

当然,还有一个创建并启动线程的函数:kthread_run

include/linux/kthread.h

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


struct task_struct *kthread_create_on_cpu(int (*threadfn)(void *data),
					  void *data,
					  unsigned int cpu,
					  const char *namefmt);

void get_kthread_comm(char *buf, size_t buf_size, struct task_struct *tsk);
bool set_kthread_struct(struct task_struct *p);

void kthread_set_per_cpu(struct task_struct *k, int cpu);
bool kthread_is_per_cpu(struct task_struct *k);

/**
 * 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;								   \
})

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

int kthread_stop(struct task_struct *thread);

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

...
int wake_up_process(struct task_struct *p)
{
	return try_to_wake_up(p, TASK_NORMAL, 0);
}
EXPORT_SYMBOL(wake_up_process);//唤醒线程
#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;								   \
})

 

因为线程也是进程,所以其结构体也是使用进程的结构体”struct task_struct”。

内核线程的退出

当线程执行到函数末尾时会自动调用内核中do_exit()函数来退出或其他线程调用kthread_stop()来指定线程退出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值