「似水」
操作系统

操作系统概述

什么是操作系统?请简要概述一下

操作系统是管理计算机硬件和软件资源的计算机程序,提供一个计算机用户与计算机硬件系统之间的接口。
向上对用户程序提供接口,向下接管硬件资源。
操作系统本质上也是一个软件,作为最接近硬件的系统软件,负责处理器管理、存储器管理、设备管理、文件管理和提供用户接口。

操作系统有哪些分类?

操作系统常规可分为批处理操作系统、分时操作系统、实时操作系统。
若一个操作系统兼顾批操作和分时的功能,则称该系统为通用操作系统。
常见的通用操作系统有:Windows、Linux、MacOS等。

什么是内核态和用户态?

为了避免操作系统和关键数据被用户程序破坏,将处理器的执行状态分为内核态和用户态。
内核态是操作系统管理程序执行时所处的状态,能够执行包含特权指令在内的一切指令,能够访问系统内所有的存储空间。
用户态是用户程序执行时处理器所处的状态,不能执行特权指令,只能访问用户地址空间。
用户程序运行在用户态,操作系统内核运行在内核态。

如何实现内核态和用户态的切换?

处理器从用户态切换到内核态的方法有三种:系统调用、异常和外部中断。

  1. 系统调用是操作系统的最小功能单位,是操作系统提供的用户接口,系统调用本身是一种软中断。
  2. 异常,也叫做内中断,是由错误引起的,如文件损坏、缺页故障等。
  3. 外部中断,是通过两根信号线来通知处理器外设的状态变化,是硬中断。

进程

什么是进程?

进程是操作系统中最重要的抽象概念之一,是资源分配的基本单位,是独立运行的基本单位。
进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文(context)中。
上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
进程一般由以下的部分组成:

  1. 进程控制块PCB,是进程存在的唯一标志,包含进程标识符PID,进程当前状态,程序和数据地址,进程优先级、CPU现场保护区(用于进程切换),占有的资源清单等。
  2. 程序段
  3. 数据段

进程的基本操作

以Unix系统举例:

  1. 进程的创建:fork()。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用 fork 时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的 PID。fork函数是有趣的(也常常令人迷惑), 因为它只被调用一次,却会返回两次:一次是在调用进程(父进程)中,一次是在新创建的子进程中。在父进程中,fork 返回子进程的 PID。在子进程中,fork 返回 0。因为子进程的 PID 总是为非零,返回值就提供一个明 确的方法来分辨程序是在父进程还是在子进程中执行。
1pid_t fork(void);
  1. 收子进程:当一个进程由于某种原因终止时,内核并不是立即把它从系统中清除。相反,进程被保持在一种已终止的状态中,直到被它的父进程回收(reaped)。当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃已终止的进程。一个进程可以通过调用waitpid 函数来等待它的子进程终止或者停止。
1pid_t waitpid(pid_t pid, int *statusp, int options);
  1. 加载并运行程序:execve 函数在当前进程的上下文中加载并运行一个新程序。
1int execve(const char *filename, const char *argv[], const char *envp[]);
  1. 进程终止:
1void exit(int status);

进程调度的时机

  1. 当前运行的进程运行结束。
  2. 当前运行的进程由于某种原因阻塞。
  3. 执行完系统调用等系统程序后返回用户进程。
  4. 在使用抢占调度的系统中,具有更高优先级的进程就绪时。
  5. 分时系统中,分给当前进程的时间片用完。

不能进行进程调度的情况

  1. 在中断处理程序执行时。
  2. 在操作系统的内核程序临界区内。
  3. 其它需要完全屏蔽中断的原子操作过程中。

进程的调度策略

  1. 先到先服务调度算法
  2. 短作业优先调度算法
  3. 优先级调度算法
  4. 时间片轮转调度算法
  5. 高响应比优先调度算法
  6. 多级队列调度算法
  7. 多级反馈队列调度算法

进程调度策略的基本设计指标

  1. CPU利用率
  2. 系统吞吐率,即单位时间内CPU完成的作业的数量。
  3. 响应时间。
  4. 周转时间。是指作业从提交到完成的时间间隔。从每个作业的角度看,完成每个作业的时间也是很关键
    • 平均周转时间
    • 带权周转时间
    • 平均带权周转时间

进程的状态与状态转换

进程在运行时有三种基本状态:就绪态、运行态和阻塞态。

  1. 运行(running)态:进程占有处理器正在运行的状态。进程已获得CPU,其程序正在执行。在单处理机系统中,只有一个进程处于执行状态; 在多处理机系统中,则有多个进程处于执行状态。
  2. 就绪(ready)态:进程具备运行条件,等待系统分配处理器以便运行的状态。 当进程已分配到除CPU以外的所有必要资源后,只要再获得CPU,便可立即执行,进程这时的状态称为就绪状态。在一个系统中处于就绪状态的进程可能有多个,通常将它们排成一个队列,称为就绪队列。
  3. 阻塞(wait)态:又称等待态或睡眠态,指进程不具备运行条件,正在等待某个时间完成的状态。

各状态之间的转换:

  1. 就绪→执行 处于就绪状态的进程,当进程调度程序为之分配了处理机后,该进程便由就绪状态转变成执行状态。
  2. 执行→就绪 处于执行状态的进程在其执行过程中,因分配给它的一个时间片已用完而不得不让出处理机,于是进程从执行状态转变成就绪状态。
  3. 执行→阻塞 正在执行的进程因等待某种事件发生而无法继续执行时,便从执行状态变成阻塞状态。
  4. 阻塞→就绪 处于阻塞状态的进程,若其等待的事件已经发生,于是进程由阻塞状态转变为就绪状 态。

什么是孤儿进程?僵尸进程?

  1. 孤儿进程: 父进程退出,子进程还在运行的这些子进程都是孤儿进程,孤儿进程将被init进程(1号进程)所收养,并由init进程对他们完成状态收集工作。
  2. 僵尸进程: 进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait 获waitpid 获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中的这些进程是僵尸进程。

什么是线程?

  1. 是进程划分的任务,是一个进程内可调度的实体,是CPU调度的基本单位,用于保证程序的实时性,实现进程内部的并发。
  2. 线程是操作系统可识别的最小执行和调度单位。每个线程都独自占用一个虚拟处理器:独自的寄存器组,指令计数器和处理器状态。
  3. 每个线程完成不同的任务,但是属于同一个进程的不同线程之间共享同一地址空间(也就是同样的动态内存,映射文件,目标代码等等),打开的文件队列和其他内核资源。

为什么需要线程?

线程产生的原因:进程可以使多个程序能并发执行,以提高资源的利用率和系统的吞吐量;但是其具有一些缺点:

  1. 进程在同一时刻只能做一个任务,很多时候不能充分利用CPU资源。
  2. 进程在执行的过程中如果发生阻塞,整个进程就会挂起,即使进程中其它任务不依赖于等待的资源,进程仍会被阻塞。

引入线程就是为了解决以上进程的不足,线程具有以下的优点:

  1. 从资源上来讲,开辟一个线程所需要的资源要远小于一个进程。
  2. 从切换效率上来讲,运行于一个进程中的多个线程,它们之间使用相同的地址空间,而且线程间彼此切换所需时间也远远小于进程间切换所需要的时间(这种时间的差异主要由于缓存的大量未命中导致)。
  3. 从通信机制上来讲,线程间方便的通信机制。对不同进程来说,它们具有独立的地址空间,要进行数据的传递只能通过进程间通信的方式进行。线程则不然,属于同一个进程的不同线程之间共享同一地址空间,所以一个线程的数据可以被其它线程感知,线程间可以直接读写进程数据段(如全局变量)来进行通信(需要一些同步措施)。

简述线程和进程的区别和联系

  1. 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程依赖于进程而存在。
  2. 进程在执行过程中拥有独立的地址空间,而多个线程共享进程的地址空间。(资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。)
  3. 进程是资源分配的最小单位,线程是CPU调度的最小单位。
  4. 通信:由于同一进程中的多个线程具有相同的地址空间,使它们之间的同步和通信的实现,也变得比较容易。进程间通信 IPC ,线程间可以直接读写进程数据段(如全局变量)来进行通信(需要一些同步方法,以保证数据的一致性)。
  5. 进程编程调试简单可靠性高,但是创建销毁开销大;线程正相反,开销小,切换速度快,但是编程调试相对复杂。
  6. 进程间不会相互影响;一个进程内某个线程挂掉将导致整个进程挂掉。
  7. 进程适应于多核、多机分布;线程适用于多核。

进程和线程的基本API

进程API以Unix系统为例,线程相关的API属于Posix线程(Pthreads)标准接口。

进程原语 线程原语 描述
fork pthread_create 创建新的控制流
exit pthread_exit 从现有的控制流中退出
waitpid pthread_join 从控制流中得到退出状态
atexit pthread_cancel_push 注册在退出控制流时调用的函数
getpid pthread_self 获取控制流的ID
abort pthread_cancel 请求控制流的非正常退出

多线程模型

  1. 多对一模型。将多个用户级线程映射到一个内核级线程上。该模型下,线程在用户空间进行管理,效率较高。缺点就是一个线程阻塞,整个进程内的所有线程都会阻塞。几乎没有系统继续使用这个模型。
  2. 一对一模型。将内核线程与用户线程一一对应。优点是一个线程阻塞时,不会影响到其它线程的执行。该模型具有更好的并发性。缺点是内核线程数量一般有上限,会限制用户线程的数量。更多的内核线程数目也给线程切换带来额外的负担。linux和Windows操作系统家族都是使用一对一模型。
  3. 多对多模型。将多个用户级线程映射到多个内核级线程上。结合了多对一模型和一对一模型的特点。

并发和并行的区别

  1. 并发(concurrency):指宏观上看起来两个程序在同时运行,比如说在单核cpu上的多任务。但是从微观上看两个程序的指令是交织着运行的,指令之间交错执行,在单个周期内只运行了一个指令。这种并发并不能提高计算机的性能,只能提高效率(如降低某个进程的相应时间)。
  2. 并行(parallelism):指严格物理意义上的同时运行,比如多核cpu,两个程序分别运行在两个核上,两者之间互不影响,单个周期内每个程序都运行了自己的指令,也就是运行了两条指令。这样说来并行的确提高了计算机的效率。所以现在的cpu都是往多核方面发展。

进程间通信

每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程A把数据从用户空间拷到内核缓冲区,进程B再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。
不同进程间的通信本质:进程之间可以看到一份公共资源;而提供这份资源的形式或者提供者不同,造成了通信方式不同。
进程间通信主要包括管道、系统IPC(包括消息队列、信号量、信号、共享内存等)、以及套接字socket。

  • 管道(Pipe)
    • 匿名管道:pipe(),只能在父子进程之间通信,单向。
    • 命名管道(FIFO):mkfifo(),可在无亲缘关系的进程间通信,持久化于文件系统。
  • 消息队列(Message Queue)
    • 基于内核的消息链表,进程可以写入/读取消息。
    • 提供消息边界,支持随机访问,适合多生产者/消费者场景。
    • 缺点:内核维护,操作相对慢。
  • 共享内存(Shared Memory)
    • 在不同进程间映射同一块物理内存。
    • 通信速度最快,适合大规模数据交换。
    • 缺点:需要配合 同步机制(信号量/互斥锁)保证并发安全。
  • 信号量(Semaphore)
    • 本质是计数器,用于进程间的同步与互斥。
    • 常和共享内存配合使用。
  • 信号(Signal)
    • 内核向进程发送的一种异步事件通知机制(如 SIGINT、SIGKILL)。
    • 常用于异常处理或事件提醒,不适合大规模数据通信。
  • 套接字(Socket)
    • 支持本机进程间和跨主机进程间通信。
    • 本地通信一般用 Unix Domain Socket,跨主机则用 TCP/UDP。
    • 通用性最强,也是分布式系统的基础。

进程通信-管道

管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。调用pipe系统函数即可 创建一个管道。有如下特质:

  1. 其本质是一个伪文件(实为内核缓冲区)
  2. 由两个文件描述符引用,一个表示读端,一个表示写端。
  3. 规定数据从管道的写端流入管道,从读端流出。

管道的原理: 管道实为内核使用环形队列机制,借助内核缓冲区实现。

管道的局限性:

  1. 数据自己读不能自己写。
  2. 数据一旦被读走,便不在管道中存在,不可反复读取。
  3. 由于管道采用半双工通信方式。因此,数据只能在一个方向上流动。
  4. 只能在有公共祖先的进程间使用管道。

匿名管道

 1int main()
 2{
 3    int fd[2];
 4    pipe(fd);
 5    pid_t pid = fork();
 6
 7    if (pid == 0) {    // 子进程
 8        close(fd[1]);  // 写端
 9
10        char buffer[8];
11        while (true) {
12            ssize_t n = read(fd[0], buffer, sizeof(buffer) - 1);
13            if (n <= 0) break;
14            buffer[n] = '\0';
15            std::cout << buffer;
16        }
17        close(fd[0]);  // 读端
18        exit(0);
19    }
20
21    // 父进程
22    close(fd[0]);  // 读端
23
24    const char* msg = "Hello from pipe!";
25    write(fd[1], msg, strlen(msg));
26    close(fd[1]);  // 写端,通知子进程 EOF
27
28    waitpid(pid, nullptr, 0);  // 等待子进程结束
29    return 0;
30}

ls | grep txt为例,

  1. Shell解析到 | ,调用 1 次 pipe()
  2. fork 并且 execve ls ,并且把 ls 的 stdout 重定向到 fd[1],关闭fd[0]
  3. fork 并且 execve grep,并把 grep 的 stdin 重定向到 fd[0],关闭fd[1]
  4. Shell 自己关闭 fd[0] 和 fd[1]

命名管道

只要存在至少一个读端和一个写端,缓冲区就有效

1mkfifo /tmp/myfifo
 1const char* FIFO_NAME = "/tmp/fifo";
 2
 3int main()
 4{
 5    mkfifo(FIFO_NAME, 0666);
 6    pid_t pid = fork();
 7
 8    if (pid == 0) {  // 子进程
 9        int fd = open(FIFO_NAME, O_RDONLY);
10
11        char buffer[8];
12        while (true) {
13            ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
14            if (n <= 0) break;
15            buffer[n] = '\0';
16            std::cout << buffer;
17        }
18        close(fd);
19        exit(0);
20    }
21
22    // 父进程
23    int fd = open(FIFO_NAME, O_WRONLY);
24
25    const char* msg = "Hello from named pipe!";
26    write(fd, msg, strlen(msg));
27    close(fd);
28
29    waitpid(pid, nullptr, 0);  // 等待子进程结束
30
31    unlink(FIFO_NAME);
32    return 0;
33}

进程通信-消息队列

特性 System V POSIX
队列管理 系统级,持久化 文件系统级,更轻量
消息结构 必须有 mtype 字段 任意数据结构
消息优先级 通过 mtype 实现 内置优先级支持
阻塞行为 可选择阻塞/非阻塞可 选择阻塞/非阻塞

消息队列可以用3个信号量实现完整的生产者-消费者模式,但是由于都有内置同步机制,通常只需要应用层的通知信号量

System V 消息队列

 1const char* PATHNAME = "/tmp";  // 路径必须存在,但文件不会被读写
 2const int PROJ_ID_MSG = 1;      // 项目标识符,同一路径下区分不同的key,0~255
 3const int PROJ_ID_SEM = 2;
 4
 5struct Msg {
 6    long mtype;       // 消息类型 >0  第一个成员必须是long
 7    char mtext[128];  // 消息内容
 8};
 9// 单条消息最大长度 /proc/sys/kernel/msgmax 默认8192
10// 总容量           /proc/sys/kernel/msgmnb 默认16384
11// 消息队列个数     /proc/sys/kernel/msgmni 默认32000
12
13void P(int semid)
14{
15    struct sembuf sb = {0, -1, 0};
16    semop(semid, &sb, 1);
17}
18
19void V(int semid)
20{
21    struct sembuf sb = {0, +1, 0};
22    semop(semid, &sb, 1);
23}
24// struct sembuf {
25//     unsigned short sem_num;   // 信号量索引(0 ~ n-1)
26//     short sem_op;             // 操作值(+/-整数)
27//     short sem_flg;            // IPC_NOWAIT  非阻塞操作
28//                               // SEM_UNDO    退出时撤销操作防止死锁
29// };
30
31int main()
32{
33    key_t key_msg = ftok(PATHNAME, PROJ_ID_MSG);  // 生成唯一 key
34    key_t key_sem = ftok(PATHNAME, PROJ_ID_SEM);
35
36    int msgid = msgget(key_msg, IPC_CREAT | 0666);  // 创建或打开队列
37    int semid = semget(key_sem, 1, IPC_CREAT | 0666);
38    semctl(semid, 0, SETVAL, 0);  // 初始值0
39
40    pid_t pid = fork();
41
42    if (pid == 0) {  // 子进程
43        Msg msg;
44        while (true) {
45            // msgtyp = 0:接收队列中最先到达的消息(FIFO),不管类型。
46            // msgtyp > 0:接收 第一个类型等于 msgtyp 的消息。
47            // msgtyp < 0:接收 第一个类型 ≤ |msgtyp| 的消息。
48            int ret = msgrcv(msgid, &msg, sizeof(msg.mtext) - 1, 0, 0);
49            if (ret >= 0) {
50                msg.mtext[ret] = '\0';
51                std::cout << msg.mtext;
52                if (strcmp(msg.mtext, "quit") == 0) break;
53                V(semid);  // 通知生产者“已处理”
54            }
55            // EIDRM 队列被删除
56            // E2BIG 有 MSG_NOERROR 数据会被截断
57        }
58        exit(0);
59    }
60
61    // 父进程
62    Msg msg;
63    msg.mtype = 1;
64
65    strcpy(msg.mtext, "Hello from msg queue with sem!\n");
66    msgsnd(msgid, &msg, strlen(msg.mtext), 0);
67    P(semid);  // 等待消费者处理完毕
68
69    strcpy(msg.mtext, "quit");
70    msgsnd(msgid, &msg, strlen(msg.mtext), 0);
71
72    waitpid(pid, nullptr, 0);  // 等待子进程结束
73
74    msgctl(msgid, IPC_RMID, nullptr);  // 回收队列
75    semctl(semid, 0, IPC_RMID);
76    return 0;
77}

POSIX 消息队列

 1const char* MQ_NAME = "/posix_mq";
 2const char* SEM_NAME = "/posix_sem";
 3const size_t MSG_SIZE = 128;
 4const int MSG_MAX = 10;
 5
 6struct Msg {
 7    char mtext[MSG_SIZE];
 8};
 9
10int main()
11{
12    struct mq_attr attr;
13    attr.mq_flags = 0;         // 阻塞模式
14    attr.mq_maxmsg = MSG_MAX;  // 最大消息数
15    attr.mq_msgsize = MSG_SIZE;
16    attr.mq_curmsgs = 0;
17
18    mqd_t mqd = mq_open(MQ_NAME, O_CREAT | O_RDWR, 0666, &attr);
19    sem_t* sem = sem_open(SEM_NAME, O_CREAT, 0666, 0);
20
21    pid_t pid = fork();
22
23    if (pid == 0) {  // 子进程
24        Msg msg;
25        while (true) {
26            ssize_t n = mq_receive(mqd, msg.mtext, MSG_SIZE, nullptr);
27            if (n >= 0) {
28                msg.mtext[n] = '\0';
29                std::cout << msg.mtext;
30                if (strcmp(msg.mtext, "quit") == 0) break;
31                sem_post(sem);  // 通知生产者已处理
32            }
33        }
34        mq_close(mqd);
35        sem_close(sem);
36        exit(0);
37    }
38
39    // 父进程:生产者
40    Msg msg;
41    strcpy(msg.mtext, "Hello POSIX message queue!\n");
42    mq_send(mqd, msg.mtext, strlen(msg.mtext), 0);
43    sem_wait(sem);  // 等待消费者处理
44
45    strcpy(msg.mtext, "quit");
46    mq_send(mqd, msg.mtext, strlen(msg.mtext), 0);
47
48    waitpid(pid, nullptr, 0);
49
50    mq_close(mqd);
51    sem_close(sem);
52    mq_unlink(MQ_NAME);
53    sem_unlink(SEM_NAME);
54    return 0;
55}

进程通信-共享内存

它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。

特点:

  1. 共享内存是最快的一种IPC,因为进程是直接对内存进行操作来实现通信,避免了数据在用户空间和内核空间来回拷贝。
  2. 因为多个进程可以同时操作,所以需要进行同步处理。
  3. 信号量和共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。

System V 共享内存

 1const char *PATHNAME = "/tmp/shm_sem";
 2const size_t BUF_SIZE = 3;
 3const size_t MSG_LEN = 64;
 4
 5enum {
 6    MUTEX = 0,
 7    FULL = 1,
 8    EMPTY = 2
 9};
10
11void P(int semid, int sem)
12{
13    struct sembuf op = {(unsigned short)sem, -1, 0};
14    semop(semid, &op, 1);
15}
16
17void V(int semid, int sem)
18{
19    struct sembuf op = {(unsigned short)sem, 1, 0};
20    semop(semid, &op, 1);
21}
22
23struct Message {
24    char data[MSG_LEN];
25};
26
27struct SharedData {
28    Message buffer[BUF_SIZE];
29    int in;   // 写指针
30    int out;  // 读指针
31};
32
33int main()
34{
35    key_t key_shm = ftok(PATHNAME, 1);
36    key_t key_sem = ftok(PATHNAME, 2);
37
38    int shm_id = shmget(key_shm, sizeof(SharedData), IPC_CREAT | 0666);
39    int sem_id = semget(key_sem, 3, IPC_CREAT | 0666);
40
41    semctl(sem_id, MUTEX, SETVAL, 1);
42    semctl(sem_id, EMPTY, SETVAL, BUF_SIZE);
43    semctl(sem_id, FULL, SETVAL, 0);
44
45    SharedData *shm_buf = (SharedData *)shmat(shm_id, nullptr, 0);
46    shm_buf->in = shm_buf->out = 0;
47
48    pid_t pid = fork();
49
50    if (pid == 0) {  // 子进程
51        for (int i = 0; i < 5; ++i) {
52            P(sem_id, FULL);
53            P(sem_id, MUTEX);
54
55            std::cout << shm_buf->buffer[shm_buf->out].data << std::endl;
56            shm_buf->out = (shm_buf->out + 1) % BUF_SIZE;
57
58            V(sem_id, MUTEX);
59            V(sem_id, EMPTY);
60        }
61        shmdt(shm_buf);
62        exit(0);
63    }
64
65    // 父进程
66    const char *msg[5] = {"msg1", "msg2", "msg3", "msg4", "msg5"};
67    for (int i = 0; i < 5; ++i) {
68        P(sem_id, EMPTY);
69        P(sem_id, MUTEX);
70
71        snprintf(shm_buf->buffer[shm_buf->in].data, MSG_LEN, "%s", msg[i]);
72        shm_buf->in = (shm_buf->in + 1) % BUF_SIZE;
73
74        V(sem_id, MUTEX);
75        V(sem_id, FULL);
76    }
77
78    waitpid(pid, nullptr, 0);
79
80    shmdt(shm_buf);
81    shmctl(shm_id, IPC_RMID, nullptr);
82    semctl(sem_id, 0, IPC_RMID);
83    return 0;
84}

POSIX 共享内存

这是POSIX共享内存+信号量 实现生产者-消费者模式的核心逻辑 我简化了错误处理来突出关键概念

工程实践中还需要添加

  1. 系统调用的错误检查
  2. 信号中断处理
  3. 资源清理的异常安全
  4. 可能的超时处理
  5. 更好的日志和调试信息
 1const char *SHM_NAME = "/shm_posix";
 2const char *SEM_MUTEX = "/sem_mutex";
 3const char *SEM_EMPTY = "/sem_empty";
 4const char *SEM_FULL = "/sem_full";
 5
 6const int BUF_SIZE = 5;
 7const int MSG_LEN = 64;
 8
 9struct Message {
10    char data[MSG_LEN];  // 序列号 时间戳 生产者ID 校验和
11};
12
13struct SharedData {
14    Message buffer[BUF_SIZE];
15    int in;   // 生产者写入位置
16    int out;  // 消费者读取位置
17};
18
19int main()
20{
21    int shm_fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
22    ftruncate(shm_fd, sizeof(SharedData));  // 设置共享内存大小
23
24    // mmap 共享内存 进程间可见 可持久化
25    SharedData *shm_buf = (SharedData *)mmap(NULL, sizeof(SharedData), PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
26    shm_buf->in = shm_buf->out = 0;
27
28    sem_t *sem_mutex = sem_open(SEM_MUTEX, O_CREAT, 0666, 1);
29    sem_t *sem_empty = sem_open(SEM_EMPTY, O_CREAT, 0666, BUF_SIZE);
30    sem_t *sem_full = sem_open(SEM_FULL, O_CREAT, 0666, 0);
31
32    pid_t pid = fork();
33
34    if (pid == 0) {
35        for (int i = 0; i < 5; ++i) {
36            sem_wait(sem_full);  // 等待有数据可读
37            sem_wait(sem_mutex);
38
39            std::cout << shm_buf->buffer[shm_buf->out].data << std::endl;
40            shm_buf->out = (shm_buf->out + 1) % BUF_SIZE;
41
42            sem_post(sem_mutex);
43            sem_post(sem_empty);  // 通知生产者有空槽位
44        }
45        exit(0);
46    }
47
48    // 父进程
49    const char *msg[5] = {"msg1", "msg2", "msg3", "msg4", "msg5"};
50    for (int i = 0; i < 5; ++i) {
51        sem_wait(sem_empty);  // 等待空槽位
52        sem_wait(sem_mutex);
53
54        snprintf(shm_buf->buffer[shm_buf->in].data, MSG_LEN, "%s", msg[i]);
55        shm_buf->in = (shm_buf->in + 1) % BUF_SIZE;
56
57        sem_post(sem_mutex);
58        sem_post(sem_full);  // 通知消费者有新数据
59    }
60
61    waitpid(pid, nullptr, 0);
62
63    munmap(shm_buf, sizeof(SharedData));
64    close(shm_fd);
65    sem_close(sem_mutex);
66    sem_close(sem_empty);
67    sem_close(sem_full);
68    shm_unlink(SHM_NAME);
69    sem_unlink(SEM_MUTEX);
70    sem_unlink(SEM_EMPTY);
71    sem_unlink(SEM_FULL);
72    return 0;
73}
  1. ftruncate的作用 “shm_open创建的共享内存对象初始大小为0,ftruncate用于设置实际需要的大小。如果不调用ftruncate就直接mmap,会因为访问未分配的内存区域而产生SIGBUS错误。”

  2. mmap vs malloc “malloc分配的是进程私有的堆内存,其他进程无法访问;而mmap可以将共享内存对象映射到进程的虚拟地址空间,多个进程可以访问同一块物理内存,这是进程间共享数据的基础。”

  3. 信号中断处理 “sem_wait等阻塞系统调用可能被信号(如SIGINT)中断,返回-1且errno为EINTR。解决方案是用do-while循环重试,或者在信号处理时使用SA_RESTART标志让系统自动重启被中断的调用。”

进程通信-信号

一个信号就是一条小消息,它通知进程系统中发生了一个某种类型的事件。 Linux 系统上支持的30 种不同类型的信号。 每种信号类型都对应于某种系统事件。低层的硬件异常是由内核异常处理程序处理的,正常情况下,对用户进程而言是不可见的。信号提供了一种机制,通知用户进程发生了这些异常。

  1. 发送信号:内核通过更新目的进程上下文中的某个状态,发送(递送)一个信号给目的进程。发送信号可以有如下两种原因:
    • 内核检测到一个系统事件,比如除零错误或者子进程终止。
    • 一个进程调用了kill 函数, 显式地要求内核发送一个信号给目的进程。一个进程可以发送信号给它自己。
  2. 接收信号:当目的进程被内核强迫以某种方式对信号的发送做出反应时,它就接收了信号。进程可以忽略这个信号,终止或者通过执行一个称为信号处理程序(signal handler)的用户层函数捕获这个信号。
1kill -l     # 查看支持的信号
信号 数字 默认动作 含义
SIGHUP 1 终止 终端挂起(控制终端关闭时发送),常用于让守护进程重新加载配置
SIGINT 2 终止 来自键盘的中断(Ctrl+C)
SIGQUIT 3 Core Dump 来自键盘的退出(Ctrl+\),并生成核心转储
SIGILL 4 Core Dump 非法指令(非法或未定义的机器指令)
SIGTRAP 5 Core Dump 调试断点或跟踪陷阱
SIGABRT 6 Core Dump 异常终止(如 abort() 调用)
SIGBUS 7 Core Dump 总线错误(访问无效的内存地址对齐)
SIGFPE 8 Core Dump 浮点异常(除零、溢出等)
SIGKILL 9 终止 强制终止,不能被捕捉/忽略
SIGUSR1 10 终止 用户自定义信号 1
SIGSEGV 11 Core Dump 段错误(非法内存访问)
SIGUSR2 12 终止 用户自定义信号 2
SIGPIPE 13 终止 向已关闭的管道写数据
SIGALRM 14 终止 定时器超时(alarm() 触发)
SIGTERM 15 终止 终止进程(可捕捉、常用于优雅退出)
SIGSTKFLT 16 终止 协处理器栈错误(很少用)
SIGCHLD 17 忽略 子进程退出或停止时发送给父进程
SIGCONT 18 继续 继续运行一个停止的进程
SIGSTOP 19 停止 立即停止,不能捕捉/忽略
SIGTSTP 20 停止 来自键盘的停止信号(Ctrl+Z)
SIGTTIN 21 停止 后台进程读终端
SIGTTOU 22 停止 后台进程写终端
信号 数字 含义
SIGPOLL / SIGIO 29 I/O 事件可用
SIGPROF 27 setitimer 的统计定时器超时
SIGWINCH 28 终端窗口大小改变
SIGVTALRM 26 虚拟时间定时器
SIGXCPU 24 超过 CPU 时间限制
SIGXFSZ 25 超过文件大小限制
SIGSYS 31 非法系统调用
SIGRTMIN~SIGRTMAX 34-64 实时信号(优先级更高,可排队)
 1void sigintHandler(int sig)
 2{
 3    std::cout << "[SIGINT] Caught signal " << sig << " (Ctrl+C pressed)" << std::endl;
 4}
 5
 6void sigusr1Handler(int sig, siginfo_t* info, void* context)
 7{
 8    (void)context;  // 避免未使用警告
 9    std::cout << "[SIGUSR1] Received from PID: " << info->si_pid << ", UID: " << info->si_uid << std::endl;
10}
11
12void alarmHandler(int sig)
13{
14    std::cout << "[SIGALRM] Alarm triggered after timeout!" << std::endl;
15}
16
17int main()
18{
19    std::cout << "PID: " << getpid() << std::endl;
20    signal(SIGINT, sigintHandler);  // 捕捉 SIGINT
21    signal(SIGPIPE, SIG_IGN);       // 忽略 SIGPIPE
22    signal(SIGALRM, alarmHandler);  // 使用 alarm 定时器
23    alarm(5);                       // 5 秒后触发 SIGALRM
24
25    // struct sigaction {
26    //     void (*sa_handler)(int);                       // 简单处理函数
27    //     void (*sa_sigaction)(int, siginfo_t*, void*);  // 高级处理函数
28    //     sigset_t sa_mask;                              // 处理期间要屏蔽的信号
29    //     int sa_flags;                                  // 行为标志位
30    // };
31    struct sigaction sa {};
32    sa.sa_flags = SA_SIGINFO;
33    // SA_SIGINFO   使用 sa_sigaction
34    // SA_RESTART   系统调用被信号中断时自动重启
35    // SA_NOCLDWAIT 避免僵尸进程(对子进程信号)
36    sa.sa_sigaction = sigusr1Handler;
37    sigaction(SIGUSR1, &sa, nullptr);  // sigaction 捕捉 SIGUSR1
38
39    sigset_t set, oldset;
40    sigemptyset(&set);
41    sigaddset(&set, SIGINT);
42    sigprocmask(SIG_BLOCK, &set, &oldset);  // 屏蔽 SIGINT 3秒
43
44    std::cout << "[INFO] SIGINT is blocked for 3 seconds, try Ctrl+C now..." << std::endl;
45    sleep(3);
46    std::cout << "[INFO] Unblocking SIGINT now." << std::endl;
47
48    sigprocmask(SIG_SETMASK, &oldset, nullptr);
49
50    while (true) pause();
51    return 0;
52}

如何编写正确且安全的信号处理函数

  1. 处理程序要尽可能简单。
  2. 在处理程序中只调用异步信号安全的函数。
  3. 保存和恢复errno。
  4. 阻塞所有的信号,保护对共享全局数据结构的访问。
  5. 用volatile 声明全局变量。
  6. 用sig_atomic_t声明标志。
  7. 信号的一个与直觉不符的方面是未处理的信号是不排队的。

解释

  1. 处理程序要尽可能简单。 避免麻烦的最好方法是保持处理程序尽可能小和简单。例如,处理程序可能只是简单地设置全局标志并立即返回;所有与接收信号相关的处理都由主程序执行,它周期性地检查(并重置)这个标志。
  2. 在处理程序中只调用异步信号安全的函数。 所谓异步信号安全的函数(或简称安全的函数)能够被信号处理程序安全地调用,原因有二:要么它是可重入的(例如只访问局部变量),要么它不能被信号处理程序中断。
  3. 保存和恢复errno。 许多Linux 异步信号安全的函数都会在出错返回时设置errno在处理程序中调用这样的函数可能会干扰主程序中其他依赖于分。解决方法是在进人处理程序时把errno 保存在一个局部变量中,在处理程序返回前恢复它。注意,只有在处理程序要返回时才有此必要。如果处理程序调用_exit终止该进程,那么就不需要这样做了。
  4. 阻塞所有的信号,保护对共享全局数据结构的访问。 如果处理程序和主程序或其他处理程序共享一个全局数据结构,那么在访问(读或者写)该数据结构时,你的处理程序和主程序应该暂时阻塞所有的信号。这条规则的原因是从主程序访问一个数据结构d 通常需要一系列的指令,如果指令序列被访问d 的处理程序中断,那么处理程序可能会发现d 的状态不一致,得到不可预知的结果。在访问d时暂时阻塞信号保证了处理程序不会中断该指令序列。
  5. 用volatile 声明全局变量。 考虑一个处理程序和一个main 函数,它们共享一个全局变量g 。处理程序更新g,main 周期性地读g, 对于一个优化编译器而言,main 中g的值看上去从来没有变化过,因此使用缓存在寄存器中g 的副本来满足对g 的每次引用是很安全的。如果这样,main 函数可能永远都无法看到处理程序更新过的值。可以用volatile 类型限定符来定义一个变量,告诉编译器不要缓存这个变量。例如:volatile 限定符强迫编译器毎次在代码中引用g时,都要从内存中读取g的值。一般来说,和其他所有共享数据结构一样,应该暂时阻塞信号,保护每次对全局变量的访问。
    1volatile int g;
  6. 用sig_atomic_t声明标志。 在常见的处理程序设计中,处理程序会写全局标志来记录收到了信号。主程序周期性地读这个标志,响应信号,再清除该标志。对于通过这种方式来共享的标志,C 提供一种整型数据类型sig_atomic_t对它的读和写保证会是原子的(不可中断的)。
  7. 信号的一个与直觉不符的方面是未处理的信号是不排队的。 因为 pending 位向量中每种类型的信号只对应有一位,所以每种类型最多只能有一个未处理的信号。关键思想是如果存在一个未处理的信号就表明至少有一个信号到达了。

进程通信-Socket

 1const char* SOCKET_PATH = "/tmp/socket";
 2
 3int main()
 4{
 5    sockaddr_un addr {};
 6    addr.sun_family = AF_UNIX;
 7    strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1);
 8
 9    int server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
10    unlink(SOCKET_PATH);  // 删除已有的 socket 文件
11    bind(server_fd, (sockaddr*)&addr, sizeof(addr));
12    listen(server_fd, 5);  // 半连接队列 最大等待客户端数量
13
14    pid_t pid = fork();
15
16    if (pid == 0) {  // 客户端
17        close(server_fd);
18
19        int sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
20        connect(sock_fd, (sockaddr*)&addr, sizeof(addr));
21        shutdown(sock_fd, SHUT_WR);  // 测试: 关闭写 半连接
22
23        char buf[128];
24        while (true) {
25            ssize_t n = read(sock_fd, buf, sizeof(buf) - 1);
26            if (n <= 0) break;
27            buf[n] = '\0';
28            std::cout << buf << std::endl;
29        }
30        close(sock_fd);
31        exit(0);
32    }
33    // 服务端
34    int client_fd = accept(server_fd, nullptr, nullptr);  // 阻塞 fcntl I/O多路复用
35
36    const char* msg = "Hello from server";
37    write(client_fd, msg, strlen(msg));
38    close(client_fd);  // 测试: 已发送数据不会丢失 客户端仍能读,TCP会根据状态机发送FIN
39
40    waitpid(pid, nullptr, 0);
41
42    close(server_fd);  // 关闭服务端 停止接收新客户端
43    unlink(SOCKET_PATH);
44    return 0;
45}

UNIX 本地 socket 通信 AF_UNIX 不走网卡,通过内核 本地IPC通道 传递数据,tcpdump和wireshark抓不到。与TCP行为一致但没有SYN、FIN包

进程同步的方法

操作系统中,进程是具有不同的地址空间的,两个进程是不能感知到对方的存在的。有时候,需要多个进程来协同完成一些任务。
当多个进程需要对同一个内核资源进行操作时,这些进程便是竞争的关系,操作系统必须协调各个进程对资源的占用,进程的互斥是解决进程间竞争关系的方法。 进程互斥指若干个进程要使用同一共享资源时,任何时刻最多允许一个进程去使用,其他要使用该资源的进程必须等待,直到占有资源的进程释放该资源。
当多个进程协同完成一些任务时,不同进程的执行进度不一致,这便产生了进程的同步问题。需要操作系统干预,在特定的同步点对所有进程进行同步,这种协作进程之间相互等待对方消息或信号的协调关系称为进程同步。进程互斥本质上也是一种进程同步。

进程的同步方法:

  1. 互斥锁
  2. 读写锁
  3. 条件变量
  4. 信号量
  5. 屏障(barrier)
  6. 记录锁(record locking)

线程同步的方法

操作系统中,属于同一进程的线程之间具有相同的地址空间,线程之间共享数据变得简单高效。遇到竞争的线程同时修改同一数据或是协作的线程设置同步点的问题时,需要使用一些线程同步的方法来解决这些问题。

线程同步的方法:

  1. 互斥锁
  2. 读写锁
  3. 条件变量
  4. 信号量
  5. 屏障(barrier)
  6. 自旋锁

进程同步与线程同步有什么区别

进程之间地址空间不同,不能感知对方的存在,同步时需要将锁放在多进程共享的空间。而线程之间共享同一地址空间,同步时把锁放在所属的同一进程空间即可。

死锁是怎样产生的?

死锁是指两个或两个以上进程在执行过程中,因争夺资源而造成的下相互等待的现象。

产生死锁需要满足下面四个条件:

  1. 互斥条件:进程对所分配到的资源不允许其他进程访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源。
  2. 占有并等待条件:进程获得一定的资源后,又对其他资源发出请求,但是该资源可能被其他进程占有,此时请求阻塞,但该进程不会释放自己已经占有的资源。
  3. 非抢占条件:进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用后自己释放。
  4. 循环等待条件:进程发生死锁后,必然存在一个进程-资源之间的环形链。

如何解决死锁问题?

解决死锁的方法即破坏产生死锁的四个必要条件之一,主要方法如下:

  1. 资源一次性分配,这样就不会再有请求了(破坏请求条件)。
  2. 只要有一个资源得不到分配,也不给这个进程分配其他的资源(破坏占有并等待条件)。
  3. 可抢占资源:即当进程新的资源未得到满足时,释放已占有的资源,从而破坏不可抢占的条件。
  4. 资源有序分配法:系统给每类资源赋予一个序号,每个进程按编号递增的请求资源,释放则相反,从而破坏环路等待的条件

优先级反转是什么?如何解决

由于多进程共享资源,具有最高优先权的进程被低优先级进程阻塞,反而使具有中优先级的进程先于高优先级的进程执行,导致系统的崩溃。这就是所谓的优先级反转(Priority Inversion)。其实,优先级反转是在高优级(假设为A)的任务要访问一个被低优先级任务(假设为C)占有的资源时,被阻塞.而此时又有优先级高于占有资源的任务(C)而低于被阻塞的任务(A)的优先级的任务(假设为B)时,于是,占有资源的任务就被挂起(占有的资源仍为它占有),因为占有资源的任务优先级很低,所以,它可能一直被另外的任务挂起.而它占有的资源也就一直不能释放,这样,引起任务A一直没办法执行.而比它优先低的任务却可以执行。

解决优先级反转普遍的2种方法:

  1. 优先级继承(priority inheritance) 优先级继承是指将低优先级任务的优先级提升到等待它所占有的资源的最高优先级任务的优先级.当高优先级任务由于等待资源而被阻塞时,此时资源的拥有者的优先级将会自动被提升。

  2. 优先级天花板(priority ceilings)优先级天花板是指将申请某资源的任务的优先级提升到可能访问该资源的所有任务中最高优先级任务的优先级.(这个优先级称为该资源的优先级天花板)。

内存管理

虚拟内存

什么是虚拟地址,什么是物理地址?

地址空间是一个非负整数地址的有序集合。
在一个带虚拟内存的系统中,CPU 从一个有N=pow(2,n)个地址的地址空间中生成虚拟地址,这个地址空间称为虚拟地址空间(virtual address space),现代系统通常支持 32 位或者 64 位虚拟地址空间。
一个系统还有一个物理地址空间(physical address space),对应于系统中物理内存的M 个字节。
地址空间的概念是很重要的,因为它清楚地区分了数据对象(字节)和它们的属性(地址)。
一旦认识到了这种区别,那么我们就可以将其推广,允许每个数据对象有多个独立的地址,其中每个地址都选自一个不同的地址空间。这就是虚拟内存的基本思想。
主存中的每字节都有一个选自虚拟地址空间的虚拟地址和一个选自物理地址空间的物理地址。

什么是虚拟内存?

为了更加有效地管理内存并且少出错,现代系统提供了一种对主存的抽象概念,叫做虚拟内存(VM)。虚拟内存是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件的完美交互,它为每个进程提供了一个大的、一致的和私有的地址空间。通过一个很清晰的机制,虚拟内存提供了三个重要的能力:

  1. 它将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,它高效地使用了主存。
  2. 它为每个进程提供了一致的地址空间,从而简化了内存管理。
  3. 它保护了每个进程的地址空间不被其他进程破坏。

为什么要引入虚拟内存?

  1. 虚拟内存作为缓存的工具
    • 虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。
    • 虚拟内存利用DRAM缓存来自通常更大的虚拟地址空间的页面。
  2. 虚拟内存作为内存管理的工具。操作系统为每个进程提供了一个独立的页表,也就是独立的虚拟地址空间。多个虚拟页面可以映射到同一个物理页面上。
    • 简化链接: 独立的地址空间允许每个进程的内存映像使用相同的基本格式,而不管代码和数据实际存放在物理内存的何处。
      • 例如:一个给定的 linux 系统上的每个进程都是用类似的内存格式,对于64为地址空间,代码段总是从虚拟地址) 0x400000 开始,数据段,代码段,栈,堆等等。
    • 简化加载: 虚拟内存还使得容易向内存中加载可执行文件和共享对象文件。要把目标文件中.text和.data节加载到一个新创建的进程中,Linux加载器为代码和数据段分配虚拟页VP,把他们标记为无效(未被缓存) ,将页表条目指向目标文件的起始位置。
      • 加载器从不在磁盘到内存实际复制任何数据,在每个页初次被引用时,虚拟内存系统会按照需要自动的调入数据页。
    • 简化共享: 独立地址空间为OS提供了一个管理用户进程和操作系统自身之间共享的一致机制。
      • 一般:每个进程有各自私有的代码,数据,堆栈,是不和其他进程共享的,这样OS创建页表,将虚拟页映射到不连续的物理页面。
      • 某些情况下,需要进程来共享代码和数据。例如每个进程调用相同的操作系统内核代码,或者C标准库函数。OS会把不同进程中适当的虚拟页面映射到相同的物理页面。
    • 简化内存分配: 虚拟内存向用户提供一个简单的分配额外内存的机制。当一个运行在用户进程中的程序要求额外的堆空间时(如 malloc ),OS分配一个适当k大小个连续的虚拟内存页面,并且将他们映射到物理内存中任意位置的k个任意物理页面,因此操作系统没有必要分配k个连续的物理内存页面,页面可以随机的分散在物理内存中。
  3. 虚拟内存作为内存保护的工具。不应该允许一个用户进程修改它的只读段,也不允许它修改任何内核代码和数据结构,不允许读写其他进程的私有内存,不允许修改任何与其他进程共享的虚拟页面。每次CPU生成一个地址时, MMU 会读一个 PTE ,通过在 PTE 上添加一些额外的许可位来控制对一个虚拟页面内容的访问十分简单。

常见的页面置换算法

当访问一个内存中不存在的页,并且内存已满,则需要从内存中调出一个页或将数据送至磁盘对换区,替换一个页,这种现象叫做缺页置换。当前操作系统最常采用的缺页置换算法如下:

  • 先进先出(FIFO)算法:

    • 思路:置换最先调入内存的页面,即置换在内存中驻留时间最久的页面。
    • 实现:按照进入内存的先后次序排列成队列,从队尾进入,从队首删除。
    • 特点:实现简单;性能较差,调出的页面可能是经常访问的
  • 最近最少使用( LRU )算法:

    • 思路: 置换最近一段时间以来最长时间未访问过的页面。根据程序局部性原理,刚被访问的页面,可能马上又要被访问;而较长时间内没有被访问的页面,可能最近不会被访问。
    • 实现:缺页时,计算内存中每个逻辑页面的上一次访问时间,选择上一次使用到当前时间最长的页面
    • 特点:可能达到最优的效果,维护这样的访问链表开销比较大
  • 最不常用算法( Least Frequently Used, LFU )

    • 思路:缺页时,置换访问次数最少的页面
    • 实现:每个页面设置一个访问计数,访问页面时,访问计数加1,缺页时,置换计数最小的页面
    • 特点:算法开销大,开始时频繁使用,但以后不使用的页面很难置换

当前最常采用的就是 LRU 算法。

请说一下什么是写时复制?

  • 如果有多个进程要读取它们自己的那部门资源的副本,那么复制是不必要的。每个进程只要保存一个指向这个资源的指针就可以了。只要没有进程要去修改自己的“副本”,就存在着这样的幻觉:每个进程好像独占那个资源。从而就避免了复制带来的负担。如果一个进程要修改自己的那份资源“副本”,那么就会复制那份资源,并把复制的那份提供给进程。不过其中的复制对进程来说是透明的。这个进程就可以修改复制后的资源了,同时其他的进程仍然共享那份没有修改过的资源。所以这就是名称的由来:在写入时进行复制。

  • 写时复制的主要好处在于:如果进程从来就不需要修改资源,则不需要进行复制。惰性算法的好处就在于它们尽量推迟代价高昂的操作,直到必要的时刻才会去执行。

  • 在使用虚拟内存的情况下,写时复制(Copy-On-Write)是以页为基础进行的。所以,只要进程不修改它全部的地址空间,那么就不必复制整个地址空间。在fork()调用结束后,父进程和子进程都相信它们有一个自己的地址空间,但实际上它们共享父进程的原始页,接下来这些页又可以被其他的父进程或子进程共享。

其他

实时操作系统的概念

实时操作系统(Real-time operating system, RTOS),又称即时操作系统,它会按照排序运行、管理系统资源,并为开发应用程序提供一致的基础。 实时操作系统与一般的操作系统相比,最大的特色就是“实时性”,如果有一个任务需要执行,实时操作系统会马上(在较短时间内)执行该任务,不会有较长的延时。这种特性保证了各个任务的及时执行。

locale

在 Docker 镜像里,很多时候你会看到:

1export LANG=C

或者

1export LANG=C.UTF-8
  • C 启动最快,不需要加载 /usr/lib/locale/… 文件,减少依赖;
  • C.UTF-8 兼顾 UTF-8 处理,避免乱码;
  • 不同机器之间结果一致,尤其是排序、正则匹配时。