Linux的进程和线程概述

版权声明:转载请声明 https://blog.csdn.net/qq_40732350/article/details/82077821

 

1.进程

1 进程的基本概念

进程是指一个具有独立功能的程序在某个数据集上的一次动态执行过程, 它是系统进行资源分配和调度的基本单元。 一次任务的运行可以并发激活多个进程, 这些进程相互合作来完成该任务的一个最终目标。


进程具有并发性、 动态性、 交互性、 独立性和异步性等主要特性。
并发性: 指的是系统中多个进程可以同时并发执行, 相互之间不受干扰。
动态性: 指的是进程都有完整的生命周期, 而且在进程的生命周期内, 进程的状态是不断变化的。另外, 进程具有动态的地址空间(包括代码、 数据和进程控制块等)。
交互性: 指的是进程在执行过程中可能会与其他进程发生直接和间接的交互操作,如进程同步和进程互斥等, 需要为此添加一定的进程处理机制。
独立性: 指的是进程是一个相对完整的资源分配和调度的基本单位, 各个进程的地址空间是相互独立的, 只有采用某些特定的通信机制才能实现进程间的通信。
异步性: 指的是每个进程都按照各自独立的、 不可预知的速度向前执行。


进程和程序是有本质区别的:

      程序是静态的一段代码, 是一些保存在非易失性存储器的指令的有序集合, 没有任何执行的概念; 而进程是一个动态的概念, 它是程序执行的过程,包括动态创建、 调度和消亡的整个过程, 它是程序执行和资源管理的最小单位。


Linux 系统中包括以下几种类型的进程。
交互式进程: 这类进程经常与用户进行交互, 因此要花很多时间等待用户的交互操作(键盘和鼠标操作等)。 当接收到用户的交互操作后, 这类进程应该很快被运行,而且响应时间的变化也应该很小, 否则用户就会觉得系统反应迟钝或者不太稳定。典型的交互式进程有 shell 命令进程、 文本编辑器和图形应用程序运行等。
批处理进程: 这类进程不必与用户进行交互, 因此经常在后台运行。 因为这类进程通常不必很快地响应, 因此往往受到调度器的“慢待”。 典型的批处理进程有编译器的编译操作、 数据库搜索引擎等。
实时进程: 这类进程通常对调度响应时间有很高的要求, 一般不会被低优先级的进程阻塞。它们不仅要求很短的响应时间,而且更重要的是响应时间的变化应该很小。典型的实时进程有视频和音频应用程序、 实时数据采集系统程序等。


2 Linux 下的进程结构

      进程不但包括程序的指令和数据,而且包括程序计数器和处理器的所有寄存器及存储临时数据的进程堆栈, 因此正在执行的进程包括处理器当前的一切活动。


       因为 Linux 是一个多进程的操作系统, 所以其他的进程必须等到系统将处理器使用权分配给自己之后才能运行。 当正在运行的进程等待其他的系统资源时, Linux 内核将取得处理器的控制权, 并将处理器分配给其他正在等待的进程, 它按照内核中的调度算法决定将处理
器分配给哪一个进程。


       内核将所有进程存放在双向循环链表(进程链表)中, 其中链表的头是 init_task 描述符。链表的每一项都是类型为 task_struct, 称为进程描述符的结构, 该结构包含了与一个进程相关的所有信息, 定义在<include/linux/sched.h>文件中。 task_struct 内核结构比较大, 它能完整地描述一个进程, 如进程的状态、 进程的基本信息、 进程标识符、 内存相关信息、 父进程相关信息、 与进程相关的终端信息、 当前工作目录、 打开的文件信息、 所接收的信号信息等。下面详细讲解 task_struct 结构中最为重要的两个域: state(进程状态) 和 pid(进程标识符)。


3. 进程的创建、 执行和终止

1) 进程的创建和执行
   许多操作系统提供的都是产生进程的机制,也就是说,首先在新的地址空间里创建进程、读入可执行文件, 最后再开始执行。 Linux 中进程的创建很特别,

它把上述步骤分解到两个单独的函数中去执行: fork()和 exec 函数族。

首先, fork()函数通过复制当前进程创建一个子进程, 子进程与父进程的区别仅仅在于不同的 PID、PPID 和某些资源及统计量。

其次,exec函数族负责读取可执行文件并将其载入地址空间开始运行。

要注意的是, Linux 中的 fork()函数使用的是写时复制页的技术, 也就是内核在创建进程时, 其资源并没有被复制过来, 资源的赋值仅仅只有在需要写入数据时才发生, 在此之前只是以只读的方式共享数据。 写时复制技术可以使 Linux 拥有快速执行的能力, 因此这个优
化是非常重要的。


2) 进程的终止
         进程终结也需要做很多烦琐的收尾工作, 系统必须保证回收进程所占用的资源, 并通知父进程。 Linux 首先把终止的进程设置为僵尸状态, 这时, 进程无法投入运行, 它的存在只为父进程提供信息, 申请死亡。 父进程得到信息后, 开始调用 wait 函数族, 最后终止子进程, 子进程占用的所有资源被全部释放。


4 进程的内存结构

        Linux 操作系统采用虚拟内存管理技术, 使得每个进程都有各自互不干涉的进程地址空间。 该地址空间是大小为 4GB 的线性虚拟空间, 用户所看到和接触到的都是该虚拟地址,无法看到实际的物理内存地址。 利用这种虚拟地址不但能起到保护操作系统的效果(用户不
能直接访问物理内存), 而且更重要的是, 用户程序可以使用比实际物理内存更大的地址空间。


       4GB 的进程地址空间会被分成两个部分: 用户空间与内核空间。

用户地址空间是从 0到 3GB(0xC0000000), 内核地址空间占据 3GB 到 4GB。

用户进程在通常情况下只能访问用户空间的虚拟地址, 不能访问内核空间的虚拟地址。 只有用户进程使用系统调用(代表用户进程在内核态执行) 时可以访问到内核空间。 每当进程切换时, 用户空间就会跟着变化;而内核空间由内核负责映射, 它并不会跟着进程改变, 是固定的。 内核空间地址有自己对应的页表, 用户进程各自有不同的页表。 每个进程的用户空间都是完全独立、 互不相干的。 进程的虚拟内存地址空间如图  所示, 其中用户空间包括以下几个功能区域。

只读段: 包含程序代码(.init 和.text) 和只读数据(.rodata)。
 数据段: 存放的是全局变量和静态变量。 其中可读可写数据段(.data) 存放已初始化的全局变量和静态变量, BSS 数据段(.bss) 存放未初始化的全局变量和静态变量。
堆: 由系统自动分配释放, 存放函数的参数值、 局部变量的值、 返回地址等。
堆栈: 存放动态分配的数据, 一般由程序员动态分配和释放。 若程序员不释放, 程序结束时可能由操作系统回收。
 共享库的内存映射区域: 这是 Linux 动态链接器和其他共享库代码的映射区域。

由于在 Linux 系统中每一个进程都会有/proc 文件系统下与之对应的一个目录(如将 init进程的相关信息在/proc/1 目录下的文件中描述), 因此通过 proc 文件系统可以查看某个进程的 地 址 空 间 的 映 射 情 况 。 例 如 , 运 行 一 个 应 用 程 序 (示 例 中 的 可 运 行 程 序 是 在/home/david/project/ 目 录 下 的 test 文 件 ), 如 果 它 的 进 程 号 为 13703 , 则 输 入 “cat/proc/13703/maps” 命令, 可以查看该进程的内存映射情况, 其结果如下:

$ cat /proc/13703/maps
/* 只读段: 代码段、 只读数据段 */
08048000-08049000 r-xp 00000000 08:01 876817 /home/david/project/test
08049000-0804a000 r--p 00000000 08:01 876817 /home/david/project/test
/* 可读写数据段 */
0804a000-0804b000 rw-p 00001000 08:01 876817 /home/david/project/test
0804b000-0804c000 rw-p 0804b000 00:00 0
08502000-08523000 rw-p 08502000 00:00 0 [heap] /* 堆 */
b7dec000-b7ded000 rw-p b7dec000 00:00 0
/* 动态共享库 */
b7ded000-b7f45000 r-xp 00000000 08:01 541691
/lib/tls/i686/cmov/libc-2.8.90.so
b7f45000-b7f47000 r--p 00158000 08:01 541691
/lib/tls/i686/cmov/libc-2.8.90.so
b7f47000-b7f48000 rw-p 0015a000 08:01 541691
/lib/tls/i686/cmov/libc-2.8.90.so
b7f48000-b7f4b000 rw-p b7f48000 00:00 0
b7f57000-b7f5a000 rw-p b7f57000 00:00 0
/* 动态链接器 */
b7f5a000-b7f74000 r-xp 00000000 08:01 524307 /lib/ld-2.8.90.so
b7f74000-b7f75000 r-xp b7f74000 00:00 0 [vdso]
b7f75000-b7f76000 r--p 0001a000 08:01 524307 /lib/ld-2.8.90.so
b7f76000-b7f77000 rw-p 0001b000 08:01 524307 /lib/ld-2.8.90.so
bff61000-bff76000 rw-p bffeb000 00:00 0 [stack] /* 堆栈 */

2 线程

       前面已经提到, 进程是系统中程序执行和资源分配的基本单位。 每个进程都拥有自己的数据段、 代码段和堆栈段, 这就造成了进程在进行切换等操作时需要较复杂的上下文切换等动作。 为了进一步减少处理机的空转时间, 支持多处理器及减少上下文切换开销, 进程在演化中出现了另一个概念——线程。 它是进程内独立的一条运行路线, 是处理器调度的最小单元, 也可以称为轻量级进程。 线程可以对进程的内存空间和资源进行访问, 并与同一进程中的其他线程共享。 因此, 线程的上下文切换的开销比创建进程小得多。


        一个进程可以拥有多个线程, 每个线程必须有一个父进程。 线程不拥有系统资源, 它只具有运行所必需的一些数据结构, 如堆栈、 寄存器与线程控制块(TCB), 线程与其父进程的其他线程共享该进程所拥有的全部资源。 要注意的是, 由于线程共享了进程的资源和地址空间, 因此, 任何线程对系统资源的操作都会给其他线程带来影响。 由此可知, 多线程中的同步是非常重要的问题。 在多线程系统中, 进程与线程的关系如图 所示。

在 Linux 系统中, 线程可以分为以下 3 种。
1. 用户级线程
      用户级线程主要解决的是上下文切换的问题, 它的调度算法和调度过程全部由用户自行选择决定, 在运行时不需要特定的内核支持。 在这里, 操作系统往往会提供一个用户空间的线程库, 该线程库提供了线程的创建、 调度和撤销等功能, 而内核仍然仅对进程进行管理。如果一个进程中的某一个线程调用了一个阻塞的系统调用函数, 那么该进程包括该进程中的其他所有线程也同时被阻塞。这种用户级线程的主要缺点是在一个进程的多个线程的调度中无法发挥多处理器的优势。
2. 轻量级进程
     轻量级进程是内核支持的用户线程, 是内核线程的一种抽象对象。 每个线程拥有一个或多个轻量级进程, 而每个轻量级进程分别被绑定在一个内核线程上。
3. 内核线程
      内核线程允许不同进程中的线程按照同一相对优先调度方法进行调度,这样就可以发挥多处理器的并发优势。
现在大多数系统都采用用户级线程与核心级线程并存的方法。一个用户级线程可以对应一个或几个核心级线程, 也就是“一对一” 或“多一” 模型。 这样既可以满足多处理器系统的需要, 也可以最大限度地减少调度开销。使用线程机制大大加快了上下文切换速度, 而且节省了很多资源。 但是因为在用户态和内核态均要实现调度管理, 所以会增加实现的复杂度和引起优先级翻转的可能性。 同时, 一个多线程程序的同步设计与调试也会增加程序实现的难度。
 

 

 

 

 

 

 

 

 


 

猜你喜欢

转载自blog.csdn.net/qq_40732350/article/details/82077821