凌云的博客

行胜于言

Linux 内核概述(四)

分类:linux| 发布时间:2025-01-24 20:16:00

Unix内核提供了一个执行环境,应用程序可以在其中运行。 因此,内核必须实现一组服务和相应的接口。应用程序通过这些接口进行交互,通常不会直接与硬件资源交互。

信号与进程间通信

Unix 信号提供了一种通知进程系统事件的机制。 每个事件都有其对应的信号编号,通常通过符号常量(如 SIGTERM)来表示。 系统事件有两种类型:

  • 异步通知
    例如,用户可以通过在终端按下中断键(通常是Ctrl-C)来向前台进程发送中断信号 SIGINT
  • 同步通知
    例如,当进程访问一个无效地址的内存位置时,内核会向该进程发送信号 SIGSEGV

POSIX标准定义了大约 20 种不同的信号,其中 2 种是用户可定义的,可以作为用户模式下进程之间通信和同步的基本机制。 一般来说,进程可以以两种方式响应信号的传递:

  • 忽略信号。
  • 异步执行指定的程序(信号处理程序)。

如果进程没有指定其中一种方式,内核会执行一个默认动作,这个动作取决于信号编号。 五种可能的默认动作是:

  • 终止进程。
  • 将执行上下文和地址空间的内容写入文件(核心转储),并终止进程。
  • 忽略信号。
  • 挂起进程。
  • 如果进程是停止状态,恢复进程的执行,。

内核信号处理相当复杂,因为 POSIX 语义允许进程暂时阻塞信号。 此外,SIGKILLSIGSTOP 信号不能由进程直接处理或忽略。

AT&T 的 Unix System V 引入了其他类型的进程间通信机制,这些机制已被许多 Unix 内核采纳:信号量、消息队列和共享内存。 它们统称为 System V IPC。

内核将这些结构实现为 IPC 资源。 进程通过调用 shmget()semget()msgget() 系统调用来获取一个资源。 像文件一样,IPC 资源是持久的:它们必须由创建进程、当前拥有者或超级用户进程显式地释放。

消息队列允许进程通过使用 msgsnd()msgrcv() 系统调用交换消息,分别向特定的消息队列中插入消息和从消息队列中提取消息。

POSIX标准(IEEE Std 1003.1-2001)定义了一种基于消息队列的 IPC 机制,通常称为 POSIX 消息队列。 它们与 System V IPC 的消息队列类似,但提供了一个更简化的基于文件的接口给应用程序。

共享内存提供了进程交换和共享数据的最快方式。 进程首先通过发出 shmget() 系统调用来创建一个具有所需大小的新共享内存。 在获得 IPC 资源标识符后,进程调用 shmat() 系统调用,返回该新区域在进程地址空间中的起始地址。 当进程希望从其地址空间中分离共享内存时,调用 shmdt() 系统调用。 共享内存的实现依赖于内核如何实现进程地址空间。

进程管理

Unix 在进程和其执行的程序之间做了明确的区分。 为此,fork()_exit() 系统调用分别用于创建新进程和终止进程,而类似 exec() 的系统调用用于加载新程序。 执行此类系统调用后,进程将在一个全新的地址空间中继续执行,该地址空间包含加载的程序。

调用 fork() 的进程是父进程,而新创建的进程是子进程。 父进程和子进程可以相互找到对方,因为描述每个进程的数据结构包括指向其直接父进程的指针以及指向所有直接子进程的指针。

fork() 的朴素实现会要求复制父进程的数据和代码,并将这些副本分配给子进程,这将非常耗时。 当前的内核可以依赖硬件分页单元,通常采用写时复制(Copy-On-Write,COW)的方法,推迟页面复制,直到最后一刻(即,直到父进程或子进程需要写入某个页面时)。

_exit() 系统调用终止一个进程。 内核通过释放进程拥有的资源并向父进程发送一个 SIGCHLD 信号来处理这个系统调用,默认情况下该信号会被忽略。

僵尸进程

父进程如何查询其子进程的终止情况? wait4() 系统调用允许进程等待直到其子进程终止;它返回已终止子进程的进程 ID(PID)。

在执行此系统调用时,内核会检查子进程是否已经终止。 引入了一种特殊的僵尸进程状态来表示已终止的进程:进程会保持这种状态,直到其父进程对其执行 wait4() 系统调用。 系统调用处理程序从进程描述符字段中提取资源使用信息;数据收集完成后,可以释放进程描述符。 如果在执行 wait4() 系统调用时没有子进程已经终止,内核通常会将该进程置于等待状态,直到有子进程终止。

许多内核还实现了 waitpid() 系统调用,它允许进程等待特定的子进程。 wait4() 系统调用的其他变体也非常常见。

内核将子进程的信息保留到父进程发出 wait4() 调用是一个好做法,但如果父进程在未发出该调用的情况下终止呢? 此时,相关信息会占用宝贵的内存空间,这些内存本可以用于服务正在运行的进程。 例如,许多 Shell 允许用户在后台启动一个命令,然后退出登录。 运行命令的 Shell 进程终止,但其子进程继续执行。

解决方案是使用一个特殊的系统进程,称为 init,它在系统初始化时创建。 当一个进程终止时,内核会改变所有已终止进程的子进程描述符指针,使它们成为 init 进程的子进程。 init 进程监控所有子进程的执行,并定期发出 wait4() 系统调用,其作用是清理所有孤立的僵尸进程。

进程组和登录会话

现代 Unix 操作系统引入了进程组的概念,用来表示“作业”(job)。 例如,为了执行以下命令行:

$ ls | sort | more

一个支持进程组的 Shell,如 bash,会为 lssortmore 三个进程创建一个新的进程组。 这样,Shell 就可以将这三个进程视为一个单一实体(即作业)。 每个进程描述符都包含一个字段,存储进程组 ID。 每个进程组可能有一个组长进程,组长进程的 PID 与进程组ID 相同。 新创建的进程最初会被插入到其父进程的进程组中。

现代 Unix 内核还引入了登录会话。 非正式地说,一个登录会话包含了所有作为特定终端上工作会话后代的进程,通常是为用户创建的第一个命令 Shell 进程。 一个进程组中的所有进程必须处于同一个登录会话中。 一个登录会话可以同时有多个进程组处于活动状态;其中一个进程组总是在前台,即它能够访问终端。 其他活动的进程组则处于后台。 当后台进程尝试访问终端时,它会收到一个 SIGTTINSIGTTOUT 信号。 在许多命令 Shell 中,内部命令 bgfg 可以用来将进程组放到后台或前台。