操作系统基础 | 2.6 linux组成概述3

内存映射(Memory Mappings)

  • mmap() 系统调用可以在进程的虚拟地址空间中创建新的内存映射。
  • 映射分为两类:
    1. 文件映射(file mapping):将文件的一部分映射到进程的虚拟内存,映射后可以像操作内存一样访问文件内容,所需页面会自动从文件加载。
    2. 匿名映射(anonymous mapping):不对应任何文件,映射区域的内容初始化为0。
  • 不同进程之间可以共享同一内存映射(如两个进程映射同一文件区域,或子进程继承父进程的映射)。
  • 如果映射是私有的(private),对映射内容的修改不会影响其他进程,也不会写回文件;如果是共享的(shared),修改会被其他进程看到,并同步到文件。
  • 内存映射的用途包括:初始化进程代码段、分配新内存、文件I/O(内存映射I/O)、进程间通信(共享映射)等。

静态库与共享库(Static and Shared Libraries)

静态库(Static libraries)

  • 静态库(archive)是包含一组已编译函数的文件,便于程序开发和维护。
  • 使用静态库时,链接器会将需要的目标模块从库中复制到最终的可执行文件中,称为静态链接
  • 典型例子:/usr/lib/libm.a(数学库的静态库,扩展名为 .a
  • 缺点:
    • 每个可执行文件都包含一份库函数代码,浪费磁盘空间。
    • 多个程序同时运行时,每个都需加载自己的库函数副本,浪费内存。
    • 如果库函数更新,所有用到该函数的程序都必须重新链接。

共享库(Shared libraries)

  • 共享库为了解决静态库的问题而设计。
  • 程序链接共享库时,链接器只在可执行文件中记录需要的共享库,运行时由动态链接器加载和链接共享库。
  • 典型例子:/lib/x86_64-linux-gnu/libm.so.6(数学库的共享库,扩展名为 .so),/lib/x86_64-linux-gnu/libc.so.6(C 标准库的共享库)
  • 优点:
    • 只需在内存中保留一份共享库代码,所有程序共享,节省内存和磁盘空间。
    • 更新共享库后,所有程序下次运行时自动使用新版本,无需重新链接。

进程间通信与同步(Interprocess Communication and Synchronization)

在运行中的 Linux 系统中,存在大量进程,其中许多进程彼此独立运行。但有些进程需要协作完成任务,这就需要进程间通信(IPC)和同步机制。

  • 最简单的通信方式是通过读写磁盘文件,但这种方式通常太慢且不灵活。

  • 因此,Linux(和所有现代 UNIX 系统)提供了丰富的进程间通信机制,包括:

    • 信号(signals):用于通知进程某个事件发生。
    • 管道(pipes)和命名管道(FIFOs):用于在进程间传递数据(如 shell 中的 | 操作符)。
    • 套接字(sockets):可用于同一主机或不同主机间的进程数据传输。
    • 文件锁(file locking):允许进程锁定文件的某些区域,防止其他进程读取或修改内容。
    • 消息队列(message queues):用于在进程间交换消息(数据包)。
    • 信号量(semaphores):用于进程间的同步操作。
    • 共享内存(shared memory):允许多个进程共享一块内存区域,任何进程对其内容的更改都能被其他进程立即看到。

信号(Signals)

  • 信号常被称为“软件中断”。信号的到来通知进程某个事件或异常情况发生。
  • 信号有多种类型,每种类型代表不同的事件或条件。每种信号类型由一个整数标识,并有类似 SIGxxxx 的符号名。
  • 信号可以由内核、其他进程(有权限时)或进程自身发送。例如,内核会在以下情况下向进程发送信号:
    • 用户在键盘上输入中断字符(通常是 Ctrl+C)
    • 进程的某个子进程终止
    • 进程设置的定时器(闹钟)到期
    • 进程试图访问无效内存地址
  • 在 shell 中,可以用 kill 命令向进程发送信号;在程序中可以用 kill() 系统调用实现同样的功能。

当进程收到信号时,会根据信号类型采取以下动作之一: - 忽略信号 - 被信号杀死 - 被挂起,直到收到特定信号后恢复

对于大多数信号类型,程序可以选择忽略信号(如果默认动作不是忽略),或者设置信号处理函数(signal handler)。信号处理函数是程序员自定义的函数,在信号送达进程时自动调用,用于处理相应的事件。

信号从产生到送达进程之间的这段时间,称为信号“挂起”(pending)。通常,挂起信号会在进程下次被调度运行时立即送达,如果进程正在运行则立即送达。但也可以通过将信号加入进程的信号屏蔽字(signal mask)来阻塞信号。如果信号在被阻塞时产生,它会一直处于挂起状态,直到被解除阻塞(即从信号屏蔽字中移除)后才送达。

线程(Threads)

  • 在现代 UNIX 实现中,每个进程可以拥有多个执行线程。
  • 可以将线程理解为一组共享同一虚拟内存(以及其他属性)的“轻量级进程”。每个线程执行相同的程序代码,并共享数据区和堆区,但每个线程有自己的栈(用于存放局部变量和函数调用信息)。
  • 线程之间可以通过共享的全局变量进行通信。线程库还提供了条件变量(condition variables)和互斥锁(mutexes)等原语,用于线程间的通信和同步,特别是对共享变量的访问控制。
  • 线程也可以使用前面介绍的进程间通信(IPC)和同步机制进行通信。
  • 使用线程的主要优点:
    • 线程间共享数据(通过全局变量)非常方便。
    • 某些算法用多线程实现比多进程实现更自然。
    • 多线程应用可以充分利用多处理器硬件,实现并行处理。

进程组与 Shell 作业控制(Process Groups and Shell Job Control)

  • shell 启动的每个程序都会在新进程中运行。例如,下面的管道命令会创建三个进程:
    1
    $ ls -l | sort -k5n | less
  • 除 Bourne shell 外,所有主流 shell 都支持“作业控制”功能,允许用户同时执行和管理多个命令或管道。
  • 在支持作业控制的 shell 中,管道中的所有进程会被放入一个新的进程组(job)中。对于单条命令,也会创建只包含一个进程的进程组。
  • 每个进程组中的进程都有相同的进程组标识符(process group ID),该 ID 通常等于组内某个进程(称为进程组领导者)的进程 ID。
  • 内核支持对进程组的操作(如发送信号),shell 利用这一特性实现对整个管道作业的挂起、恢复等控制。