前段实践在B站进行模拟面试时发现,
模拟面试第四期-已经拿到大厂OFFER的研究生大佬-LINUX卷到飞起
自己对Linux中的同步互斥方法,以及IPC方法,没有很好的理解和总结过。因此,本笔记将总结这部分内容。
内核 | 线程 | 进程 | |
---|---|---|---|
机制 | 原子操作、自旋锁、信号量、互斥量 | 互斥量、读写锁、条件变量、信号量(有些书把信号量放在IPC通信里面,因为它可以被用于进程间通信同步) | IPC:管道(无名管道)、FIFO(有名管道)、消息队列、信号(Signals)、共享内存、套接字 (Sockets) |
特点 | 提供了多种用于进程间通信和线程同步的基础设施 | 协调同一进程中不同线程对共享数据的访问 | 在不同的进程之间传递数据 |
1.内核解决并发与竞态的方法
这个部分在Linux内核学习笔记中,已经做过关于内部实现的笔记。
Linux驱动-同步互斥与原子变量 - 认真学习的小诚同学
Linux驱动-内核锁的介绍与使用 - 认真学习的小诚同学
Linux驱动-内核自旋锁spinlock的实现 - 认真学习的小诚同学
Linux驱动-内核信号量semaphore的实现 - 认真学习的小诚同学
Linux驱动-内核互斥量mutex的实现 - 认真学习的小诚同学
这里做个总结:
内核锁分为
- 自旋锁(无法获得锁时,当前线程原地等待):原始自旋锁(raw_spinlock_t)、位自旋锁(bit spinlocks)
- 睡眠锁(无法获得锁时,当前线程就会休眠):互斥量、实时互斥锁、信号量、读写信号量等。
机制 | 实现方式 |
---|---|
自旋锁 | UP(单CPU):支持抢占的系统,关闭调度器;不支持抢占的系统,本身就独占了 SMP(多核):使用原子操作ldrex和strex完成互斥 |
信号量 | 通过自旋锁lock和计数量count,以及一个等待队列(阻塞的线程放在其中) |
互斥量 | 同样通过自旋锁lock和二值计数量count,以及一个等待队列(阻塞的线程放在其中) 实现了一个快速路径fastpath-在大部分情况下都可以直接在此获得或释放锁,所以互斥量比信号量的效率高 |
2.线程间同步
线程同步(synchronization)是指在一定的时间内只允许某一个线程访问某个共享资源。而在此时间内,不允许其它的线程访问该资源。
2.1 互斥量
从本质上来说是一把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量上的锁。
2.1.1 API
功能 | API | 描述 |
---|---|---|
创建 | int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); | 返回:若成功返回0,否则返回错误编号 |
销毁 | int pthread_mutex_destroy(pthread_mutex_t mutex); | 返回:若成功返回0,否则返回错误编号 |
加锁 | int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_trylock(pthread_mutex_t *mutex); | 返回:若成功返回0,否则返回错误编号; 调用pthread_mutex_lock,如果互斥量已经上锁, 调用线程阻塞直到互斥量被解锁。 如果不希望被阻塞,可以使用pthread_mutex_trylock 尝试加锁,无论成功都会返回。 |
解锁 | int pthread_mutex_unlock(pthread_mutex_t *mutex); |
2.1.2 死锁问题
死锁详解和解决办法_避免死锁的三种方法-CSDN博客
【并发 bug 和应对 (死锁/数据竞争/原子性违反;防御性编程和动态分析) 南京大学2022操作系统-P8】 【精准空降到 24:22】
死锁只有同时满足以下四个条件才会发生:
- 互斥:一个资源每次只能被一个进程使用;
- 持有并等待:一个进程请求资源阻塞时,不释放已获得的资源;
- 不可剥夺:进程已获得的资源不能被强行剥夺;
- 环路等待:若干进程之间形成头尾相接的循环等待资源关系。
解决方法就是经典的银行家算法。
2.1.3 示例
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>#define NUM_THREADS 5// 共享资源
int shared_counter = 0;
// 互斥锁
pthread_mutex_t mutex;void* thread_function(void* arg) {int id = *((int*)arg);free(arg);for (int i = 0; i < 5; i++) {// 加锁pthread_mutex_lock(&mutex);// 访问共享资源int local_counter = shared_counter;printf("Thread %d read shared_counter: %d\n", id, local_counter);local_counter++;printf("Thread %d incremented shared_counter to: %d\n", id, local_counter);// 更新共享资源shared_counter = local_counter;// 解锁pthread_mutex_unlock(&mutex);// 模拟一些其他操作和等待sleep(1);}return NULL;
}int main() {pthread_t threads[NUM_THREADS];// 初始化互斥锁pthread_mutex_init(&mutex, NULL);// 创建线程for (int i = 0; i < NUM_THREADS; i++) {int* id = malloc(sizeof(int));*id = i + 1;pthread_create(&threads[i], NULL, thread_function, id);}// 等待所有线程完成for (int i = 0; i < NUM_THREADS; i++) {pthread_join(threads[i], NULL);}// 销毁互斥锁pthread_mutex_destroy(&mutex);// 打印最后的共享计数器值printf("Final value of shared_counter: %d\n", shared_counter);return 0;
}
2.2 读写锁
与互斥量类似,但读写锁允许更高的并行性。其特性为:写独占,读共享。
适用场景:读写锁非常适合于对数据结构读的次数远大于写的情况。
2.2.1 API
功能 | API | 描述 |
---|---|---|
创建 | int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr); | 返回:若成功返回0,否则返回错误编号 |
销毁 | int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); | 返回:若成功返回0,否则返回错误编号 |
加锁 | int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); | 返回:若成功返回0,否则返回错误编号; |
解锁 | int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); |
2.2.2 示例
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>#define NUM_READERS 5
#define NUM_WRITERS 2pthread_rwlock_t rwlock;
int shared_data = 0;void* reader(void* arg) {int id = *((int*)arg);free(arg);while (1) {pthread_rwlock_rdlock(&rwlock);printf("Reader %d: read shared_data = %d\n", id, shared_data);pthread_rwlock_unlock(&rwlock);sleep(1);}return NULL;
}void* writer(void* arg) {int id = *((int*)arg);free(arg);while (1) {pthread_rwlock_wrlock(&rwlock);shared_data++;printf("Writer %d: updated shared_data to %d\n", id, shared_data);pthread_rwlock_unlock(&rwlock);sleep(2);}return NULL;
}int main() {pthread_t readers[NUM_READERS], writers[NUM_WRITERS];pthread_rwlock_init(&rwlock, NULL);for (int i = 0; i < NUM_READERS; i++) {int* id = malloc(sizeof(int));*id = i + 1;pthread_create(&readers[i], NULL, reader, id);}for (int i = 0; i < NUM_WRITERS; i++) {int* id = malloc(sizeof(int));*id = i + 1;pthread_create(&writers[i], NULL, writer, id);}for (int i = 0; i < NUM_READERS; i++) {pthread_join(readers[i], NULL);}for (int i = 0; i < NUM_WRITERS; i++) {pthread_join(writers[i], NULL);}pthread_rwlock_destroy(&rwlock);return 0;
}
2.3 信号量
可以理解为一个计数器,表示当前可用共享资源的数量。
-
信号量基于操作系统的 PV 操作,对信号量的操作都是原子操作;
-
信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数;
-
支持信号量组;
-
有两套信号量:System V 和 Semaphore
2.3.1 API
<sys/sem.h>
- System V 信号量
-
特性:
-
属于System V IPC(进程间通信)。
-
提供了一组复杂的功能,适合复杂的进程间同步控制。
-
典型操作包括
semget()
(获取/创建信号量)、semop()
(操作信号量)和semctl()
(控制信号量集)。 -
支持信号量集的概念,可以一次性操作多个信号量,适用于高级同步需求。
-
int semget(key_t key, int num_sems, int sem_flags);
// 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1int semctl(int semid, int sem_num, int cmd, ...);
// 控制信号量的相关信息int semop(int semid, struct sembuf semoparray[], size_t numops);
// 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
-
-
使用场景:
- 适用于需要细粒度、复杂同步的多进程应用程序。
- 在需要跨越不同应用程序的持久性IPC时(System V信号量可以在系统重启后仍保持存在,直到显式删除)。
-
复杂性:
- 相对复杂,使用起来需要理解其数据结构和操作指标。
- 相关数据结构和操作通常需要更复杂的设置和管理。需要使用
union semun
进行一些配置,这是标准的一个例外之处。
<semaphore.h>
- POSIX 信号量
- 特性:
- 属于POSIX标准。
- 提供了更简单易用的信号量API。典型操作包括
sem_init()
(初始化信号量)、sem_wait()
(等待信号量)、sem_post()
(释放信号量)和sem_destroy()
(销毁信号量)。 - 可以是命名信号量或无名信号量。命名信号量可以用于进程间同步,而无名信号量通常用于线程间同步。
- 使用场景:
- 适用于进程间或线程间的简单同步。
- 易于使用,集成到标准C库中,大多数现代系统都支持。
- 复杂性:
- 易于使用和管理,适合需要简单同步机制的应用。
- 不支持信号量集,但更适合于一般的多线程和多进程同步场景。
2.3.2 示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <semaphore.h>
#include <sys/wait.h>
#include <fcntl.h> // For O_CREATint main() {pid_t pid;sem_t *sem;// 创建一个命名信号量,初始值为0sem = sem_open("/example_semaphore", O_CREAT, 0644, 0);if (sem == SEM_FAILED) {perror("sem_open");exit(EXIT_FAILURE);}pid = fork();if (pid < 0) {perror("fork");exit(EXIT_FAILURE);} else if (pid == 0) {// 子进程部分printf("子进程: 正在执行任务...\n");sleep(2); // 模拟某些工作的延迟printf("子进程: 完成任务!\n");// 子进程完成任务后,释放信号量sem_post(sem);// 关闭子进程中的信号量sem_close(sem);exit(EXIT_SUCCESS);} else {// 父进程部分printf("父进程: 等待子进程完成...\n");// 等待子进程完成信号量操作sem_wait(sem);printf("父进程: 收到子进程完成信号,继续执行...\n");// 等待子进程退出wait(NULL);// 关闭父进程中的信号量sem_close(sem);// 移除命名信号量sem_unlink("/example_semaphore");exit(EXIT_SUCCESS);}
}
2.4 条件变量
条件变量是一种同步机制,允许线程挂起,直到共享数据上的某些条件得到满足。
主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使“条件成立”.
为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。
2.4.1 API
2.4.2 示例
#include <pthread.h>
#include <stdio.h>#define BUFFER_SIZE 10
int buffer[BUFFER_SIZE];
int count = 0;pthread_mutex_t mutex;
pthread_cond_t cond;void* producer(void* arg) {for (int i = 0; i < 20; i++) {pthread_mutex_lock(&mutex);while (count == BUFFER_SIZE) {pthread_cond_wait(&cond, &mutex);}buffer[count++] = i;printf("Produced: %d\n", i);pthread_cond_signal(&cond);pthread_mutex_unlock(&mutex);}return NULL;
}void* consumer(void* arg) {for (int i = 0; i < 20; i++) {pthread_mutex_lock(&mutex);while (count == 0) {pthread_cond_wait(&cond, &mutex);}int item = buffer[--count];printf("Consumed: %d\n", item);pthread_cond_signal(&cond);pthread_mutex_unlock(&mutex);}return NULL;
}int main() {pthread_t prod, cons;pthread_mutex_init(&mutex, NULL);pthread_cond_init(&cond, NULL);pthread_create(&prod, NULL, producer, NULL);pthread_create(&cons, NULL, consumer, NULL);pthread_join(prod, NULL);pthread_join(cons, NULL);pthread_mutex_destroy(&mutex);pthread_cond_destroy(&cond);return 0;
}
3.进程间通信
参考:一文搞懂六大进程通信机制原理(全网最详细)
3.1 管道
通常指无名管道。
- 数据只能在一个方向上流动, 具有固定的读端和写端
- 它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)
3.1.1 API
#include <unistd.h>
int pipe(int filedes[2]);
返回值:若成功则返回0,若出错则返回-1; 说明:由参数filedes返回两个文件描述符:filedes[0]为读而打开;filedes[1]为写而打开。
3.1.2 示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>int main() {int pipefd[2];pid_t pid;char writeMessage[] = "Hello from parent!";char readBuffer[100];// 创建管道,pipefd[0]用于读,pipefd[1]用于写if (pipe(pipefd) == -1) {perror("pipe");exit(EXIT_FAILURE);}// 创建子进程pid = fork();if (pid == -1) {perror("fork");exit(EXIT_FAILURE);}if (pid == 0) {// 子进程执行此块// 关闭写入端close(pipefd[1]);// 从管道中读取数据read(pipefd[0], readBuffer, sizeof(readBuffer));printf("子进程接收到: %s\n", readBuffer);// 关闭读取端close(pipefd[0]);_exit(EXIT_SUCCESS);} else {// 父进程执行此块// 关闭读取端close(pipefd[0]);// 向管道中写入数据write(pipefd[1], writeMessage, strlen(writeMessage) + 1);// 关闭写入端close(pipefd[1]);// 等待子进程完成wait(NULL);exit(EXIT_SUCCESS);}
}
3.2 FIFO
命名管道,它是一种文件类型。
- FIFO可以在无关的进程之间交换数据,与无名管道不同。
- FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
返回值:成功返回0,出错返回-1;
其中的 mode 参数与open函数中的 mode 相同。一旦创建了一个 FIFO,就可以用一般的文件I/O函数操作它。
示例:
// 发送进程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>#define FIFO_NAME "myfifo"int main() {int fd;char message[] = "Hello from FIFO sender!";// 创建FIFO,如果已经存在则不会重新创建if (mkfifo(FIFO_NAME, 0666) == -1) {perror("mkfifo");exit(EXIT_FAILURE);}// 打开FIFO以写入fd = open(FIFO_NAME, O_WRONLY);if (fd == -1) {perror("open");exit(EXIT_FAILURE);}// 写入数据到FIFOwrite(fd, message, strlen(message) + 1);printf("发送: %s\n", message);// 关闭FIFOclose(fd);// 删除FIFOunlink(FIFO_NAME);return 0;
}
// 接受进程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>#define FIFO_NAME "myfifo"int main() {int fd;char buffer[100];// 打开FIFO以读取fd = open(FIFO_NAME, O_RDONLY);if (fd == -1) {perror("open");exit(EXIT_FAILURE);}// 从FIFO读取数据read(fd, buffer, sizeof(buffer));printf("接收: %s\n", buffer);// 关闭FIFOclose(fd);return 0;
}
3.3 消息队列
消息队列就是消息的链表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。
- 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级;
- 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除;
- 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
3.3.1 API
#include <sys/msg.h>
int msgget(key_t key, int flag); // 创建或打开消息队列:成功返回队列ID,失败返回-1
int msgctl(int msqid, int cmd, struct msqid_ds *buf); // 控制消息队列:成功返回0,失败返回-1
int msgsnd(int msqid, const void *ptr, size_t size, int flag); // 添加消息:成功返回0,失败返回-1
int msgrcv(int msqid, void *ptr, size_t size, long type,int flag); // 读取消息:成功返回消息数据的长度,失败返回-1
在以下两种情况下,msgget将创建一个新的消息队列:
- 如果没有与键值key相对应的消息队列,并且flag中包含了IPC_CREATE
- key参数为IPC_PRIVATE
函数msgrcv在读取消息队列时,type参数有下面几种情况:
- type == 0,返回队列中的第一个消息;
- type > 0,返回队列中消息类型为 type 的第一个消息;
- type < 0,返回队列中消息类型值小于或等于 type 绝对值的消息,如果有多个,则取类型值最小的消息。
服务端程序一直在等待特定类型的消息,当收到该类型的消息以后,发送另一种特定类型的消息作为反馈,客户端读取该反馈并打印出来
3.3.2 示例
服务端程序一直在等待特定类型的消息,当收到该类型的消息以后,发送另一种特定类型的消息作为反馈,客户端读取该反馈并打印出来。
// server.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>#define QUEUE_KEY 1234
#define MSG_TYPE_REQUEST 1
#define MSG_TYPE_RESPONSE 2struct message {long msg_type;char msg_text[100];
};int main() {int msgid;struct message msg;// 创建消息队列msgid = msgget(QUEUE_KEY, 0666 | IPC_CREAT);if (msgid == -1) {perror("msgget");exit(EXIT_FAILURE);}while (1) {// 等待接收特定类型的消息if (msgrcv(msgid, &msg, sizeof(msg.msg_text), MSG_TYPE_REQUEST, 0) == -1) {perror("msgrcv");exit(EXIT_FAILURE);}printf("服务端接收到: %s\n", msg.msg_text);// 发送反馈消息msg.msg_type = MSG_TYPE_RESPONSE;snprintf(msg.msg_text, sizeof(msg.msg_text), "Acknowledged: %s", msg.msg_text);if (msgsnd(msgid, &msg, sizeof(msg.msg_text), 0) == -1) {perror("msgsnd");exit(EXIT_FAILURE);}}// 删除消息队列(一般情况下,不会到这一步,因为服务端通常持续运行)msgctl(msgid, IPC_RMID, NULL);return 0;
}
// client.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>#define QUEUE_KEY 1234
#define MSG_TYPE_REQUEST 1
#define MSG_TYPE_RESPONSE 2struct message {long msg_type;char msg_text[100];
};int main() {int msgid;struct message msg;// 获取消息队列msgid = msgget(QUEUE_KEY, 0666);if (msgid == -1) {perror("msgget");exit(EXIT_FAILURE);}// 准备和发送请求消息msg.msg_type = MSG_TYPE_REQUEST;snprintf(msg.msg_text, sizeof(msg.msg_text), "Hello, Server!");if (msgsnd(msgid, &msg, sizeof(msg.msg_text), 0) == -1) {perror("msgsnd");exit(EXIT_FAILURE);}// 等待接收服务端的反馈消息if (msgrcv(msgid, &msg, sizeof(msg.msg_text), MSG_TYPE_RESPONSE, 0) == -1) {perror("msgrcv");exit(EXIT_FAILURE);}printf("客户端接收到反馈: %s\n", msg.msg_text);return 0;
}
3.4 信号
信号和信号量是完全不同的两个概念
3.4.1 有哪些信号
kill -l // 罗列出系统中的信号1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
Linux 系统中有许多信号,其中前面 31 个信号都有一个特殊的名字,对应一个特殊的事件,这些信号都是从 Unix 系统继承下来的,他们还有个名称叫**“不可靠信号”**,他们有如下的特点:
- 非实时信号不排队,信号的响应会相互嵌套。
- 如果目标进程没有及时响应非实时信号,那么随后到达的该信号将会被丢弃。
- 每一个非实时信号都对应一个系统事件,当这个事件发生时,将产生这个信号。
- 如果进程的挂起信号中含有实时和非实时信号,那么进程优先响应实时信号并且会从大到小依此响应,而非实时信号没有固定的次序。
后面的 31 个信号(从 SIGRTMIN[34]
到 SIGRTMAX[64]
)是 Linux 系统新增的实时信号,也被称为**“可靠信号”**,这些信号的特征是:
- 实时信号的响应次序按接收顺序排队,不嵌套。
- 即使相同的实时信号被同时发送多次,也不会被丢弃,而会依次挨个响应。
- 实时信号没有特殊的系统事件与之对应
对以上信号,需要着重注意的是:
1,上表中罗列出来的信号的“值”,在 x86、PowerPC 和 ARM
平台下是有效的,但是别的平台的信号值也许跟这个表的不一致。
2,“备注”中注明的事件发生时会产生相应的信号,但并不是说该信号的产生就一定发生了这个事件。事实上,任何进程都可以使用函数 kill()
来产生任何信号。
3,信号 SIGKILL
和 SIGSTOP
是两个特殊的信号,他们不能被忽略、阻塞或捕捉,只能按缺省动作来响应。
换句话说,除了这两个信号之外的其他信号,接收信号的目标进程按照如下顺序来做出反应:
A) 如果该信号被阻塞,那么将该信号挂起,不对其做任何处理,等到解除对其阻塞为止。否则进入B。
B) 如果该信号被捕捉,那么进一步判断捕捉的类型:
B1) 如果设置了响应函数,那么执行该响应函数。
B2) 如果设置为忽略,那么直接丢弃该信号。
否则进入C。
C) 执行该信号的缺省动作
3.4.2 API
signal
函数:- 用于设置一个简单的信号处理器。
- 原型:
void (*signal(int sig, void (*func)(int)))(int);
- 使用时,指定要捕获的信号以及处理信号的函数。
sigaction
函数:- 提供了更强大和灵活的信号处理设置。
- 原型:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
- 可以将
struct sigaction
的sa_handler
设置为SIG_IGN
来忽略信号或SIG_DFL
恢复默认处理。 - 提供了一种可靠的方法来设置信号处理程序,并支持更多功能,如信号阻塞集。
raise
函数:- 发送信号给调用进程本身。
- 原型:
int raise(int sig);
- 实际上相当于调用
kill(getpid(), sig);
。
kill
函数:- 发送信号给指定进程。
- 原型:
int kill(pid_t pid, int sig);
- 可以用来向一个或多个进程发送信号。
……
3.5 共享内存
3.5 共享内存
指两个或多个进程共享一个给定的存储区.
- 共享内存是最快的一种 IPC,因为进程是直接对内存进行存取
- 因为多个进程可以同时操作,所以需要进行同步
- 信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问
3.5.1 API
#include <sys/shm.h>
int shmget(key_t key, size_t size, int flag); // 创建或获取一个共享内存:成功返回共享内存ID,失败返回-1
int shmctl(int shm_id, int cmd, struct shmid_ds *buf); // 控制共享内存的相关信息:成功返回0,失败返回-1
void *shmat(int shm_id, const void *addr, int flag); // 连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1
int shmdt(void *addr); // 断开与共享内存的连接:成功返回0,失败返回-1
当用shmget函数创建一段共享内存时,必须指定其size;而如果引用一个已存在的共享内存,将size 指定为0。
当一段共享内存被创建以后,它并不能被任何进程访问。必须使用shmat函数连接该共享内存到当前进程的地址空间,随后可以访问。
shmdt函数是用来断开shmat建立的连接的。注意,这并不是从系统中删除该共享内存,只是当前进程不能再访问该共享内存而已。
shmctl函数可以对共享内存执行多种操作,根据参数 cmd 执行相应的操作。常用的是IPC_RMID(从系统中删除该共享内存)。
3.5.2 示例
【共享内存+信号量+消息队列】:
- 消息队列将用于初始化通信。在这种情况下,客户端可以请求服务,同时也可以用来通知服务器有新的请求需要处理。
- 共享内存将作为数据交换的主要手段,因为它允许两个进程共享和访问同一内存段。
- 信号量用于同步,以便控制对共享内存的访问,防止竞态条件。
// server.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/msg.h>
#include <string.h>
#include <unistd.h>#define SHM_KEY 0x1234
#define SEM_KEY 0x5678
#define MSG_KEY 0x9abc
#define SHM_SIZE 1024struct message {long msg_type;int client_pid;
};union semun {int val;struct semid_ds *buf;unsigned short *array;
};void handle_client_request(int semid, char *shared_mem, int client_pid) {struct sembuf sb;// Wait (P operation)sb.sem_num = 0;sb.sem_op = -1;sb.sem_flg = 0;if (semop(semid, &sb, 1) == -1) {perror("semop - wait");exit(EXIT_FAILURE);}// 读取客户端的数据printf("服务器读取: %s\n", shared_mem);// 向共享内存中写入处理后的回应snprintf(shared_mem, SHM_SIZE, "服务器已处理请求,收到了: %s", shared_mem);// Signal (V operation)sb.sem_op = 1;if (semop(semid, &sb, 1) == -1) {perror("semop - signal");exit(EXIT_FAILURE);}// 向客户端发送处理完成的通知printf("服务器处理完毕,通知客户端 PID: %d\n", client_pid);
}int main() {int shmid, semid, msgid;char *shared_mem;struct message msg;union semun sem_union;struct sembuf sb;// 创建共享内存shmid = shmget(SHM_KEY, SHM_SIZE, 0666 | IPC_CREAT);if (shmid == -1) {perror("shmget");exit(EXIT_FAILURE);}// 附加共享内存shared_mem = (char *)shmat(shmid, NULL, 0);if (shared_mem == (char *)-1) {perror("shmat");exit(EXIT_FAILURE);}// 创建信号量semid = semget(SEM_KEY, 1, 0666 | IPC_CREAT);if (semid == -1) {perror("semget");exit(EXIT_FAILURE);}// 初始化信号量为1sem_union.val = 1;if (semctl(semid, 0, SETVAL, sem_union) == -1) {perror("semctl");exit(EXIT_FAILURE);}// 创建消息队列msgid = msgget(MSG_KEY, 0666 | IPC_CREAT);if (msgid == -1) {perror("msgget");exit(EXIT_FAILURE);}// 服务器循环接收请求while (1) {// 接收来自客户端的请求消息if (msgrcv(msgid, &msg, sizeof(int), 1, 0) == -1) {perror("msgrcv");exit(EXIT_FAILURE);}printf("服务器收到来自客户端的请求,PID: %d\n", msg.client_pid);// 处理客户端请求handle_client_request(semid, shared_mem, msg.client_pid);}return 0;
}
// client.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/msg.h>
#include <string.h>
#include <unistd.h>#define SHM_KEY 0x1234
#define SEM_KEY 0x5678
#define MSG_KEY 0x9abc
#define SHM_SIZE 1024struct message {long msg_type;int client_pid;
};union semun {int val;struct semid_ds *buf;unsigned short *array;
};int main() {int shmid, semid, msgid;char *shared_mem;struct message msg;union semun sem_union;struct sembuf sb;pid_t pid = getpid();// 获取共享内存shmid = shmget(SHM_KEY, SHM_SIZE, 0666);if (shmid == -1) {perror("shmget");exit(EXIT_FAILURE);}// 附加共享内存shared_mem = (char *)shmat(shmid, NULL, 0);if (shared_mem == (char *)-1) {perror("shmat");exit(EXIT_FAILURE);}// 获取信号量semid = semget(SEM_KEY, 1, 0666);if (semid == -1) {perror("semget");exit(EXIT_FAILURE);}// 获取消息队列msgid = msgget(MSG_KEY, 0666);if (msgid == -1) {perror("msgget");exit(EXIT_FAILURE);}// 准备发送请求msg.msg_type = 1;msg.client_pid = pid;// Wait (P operation)sb.sem_num = 0;sb.sem_op = -1;sb.sem_flg = 0;if (semop(semid, &sb, 1) == -1) {perror("semop - wait");exit(EXIT_FAILURE);}// 向共享内存中写入请求snprintf(shared_mem, SHM_SIZE, "请求数据来自客户端 PID: %d", pid);printf("客户端发送: %s\n", shared_mem);// Signal (V operation)sb.sem_op = 1;if (semop(semid, &sb, 1) == -1) {perror("semop - signal");exit(EXIT_FAILURE);}// 发送请求消息到服务端if (msgsnd(msgid, &msg, sizeof(int), 0) == -1) {perror("msgsnd");exit(EXIT_FAILURE);}// 等待服务端处理sleep(1); // 这里使用睡眠进行简单的等待,实践中应采用更好的同步机制// Signal again to re-check shared memory// 这里选择重新检入共享内存内容前的信号处理,依赖于应用具体设计// Read the server responseprintf("客户端收到: %s\n", shared_mem);return 0;
}
3.6 套接字
Socket支持不同主机上的两个进程IPC。这里不详细介绍了,可以参考网络编程相关知识。