IPC(InterProcess Communication)的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中Socket和Stream支持不同主机上的两个进程IPC。
-
管道(Pipes):管道是一种半双工的通信方式,用于具有亲缘关系的进程间通信。它通常用于父子进程或者兄弟进程之间。管道可以是匿名管道,也可以是命名管道。
-
消息队列(Message Queues):消息队列是一种通过消息传递进行通信的方式。发送方将消息发送到队列中,接收方从队列中接收消息。消息队列可以实现进程间的异步通信。
-
信号量(Semaphores):信号量是一种计数器,用于控制对共享资源的访问。它通常用于同步进程之间的操作,以避免竞争条件。
-
共享内存(Shared Memory):共享内存是一种允许多个进程访问同一块内存区域的方式。这种方式通常比较高效,但需要处理进程间的同步和互斥。
-
套接字(Sockets):套接字是一种网络编程接口,不仅可以用于不同主机间的进程通信,也可以用于同一主机上的进程通信。套接字可以基于网络协议(如TCP/IP)或本地协议(如UNIX域套接字)实现。
一、管道
管道(Pipes)是一种在Unix和类Unix系统中常见的进程间通信(IPC)机制,用于在具有亲缘关系的进程之间传递数据。管道是一个单向通道,允许一个进程将输出直接发送到另一个进程的输入。它是一种半双工通信方式,即数据只能单向流动,不能双向传输。
类型
-
匿名管道(Anonymous Pipes):匿名管道是最简单的管道形式,它只存在于内存中,并且通常用于父子进程之间的通信。在Unix系统中,可以使用
pipe()
系统调用创建匿名管道。 -
命名管道(Named Pipes):命名管道是一种具有持久性的管道,它以文件的形式存在于文件系统中,并允许无关进程之间进行通信。命名管道通常用于不具有亲缘关系的进程之间的通信。
特点
-
单向通信:管道是单向的,数据只能沿着管道的方向流动,不能双向传输。
-
半双工:管道是半双工的,即数据只能在一个方向上传输。如果需要双向通信,通常需要创建两个管道。
-
FIFO(先进先出):管道遵循FIFO的原则,即数据按照写入的顺序从管道中读取出来。
使用
在Unix系统中,可以使用pipe()
系统调用创建匿名管道,它返回两个文件描述符,一个用于读取,一个用于写入。然后可以使用fork()
创建一个新的进程,在父子进程之间共享管道,并使用dup2()
系统调用将管道文件描述符重定向到标准输入或标准输出。接着,一个进程可以通过写入管道的方式向另一个进程发送数据,另一个进程则可以通过读取管道来接收数据。
示例
下面是一个简单的C语言示例,演示了如何在父子进程之间使用匿名管道进行通信:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
int fd[2];
int pid;
char buf[128];
//创建管道
if(pipe(fd) == -1) {
printf("creat pipe failed\n");
}
//创建子进程
pid = fork();
if(pid < 0) {
printf("creat child faild\n");
} else if(pid > 0) { //父进程
printf("this is father\n");
close(fd[0]); //关闭读取端
//向管道写数据
write(fd[1], "hello from father", strlen("hello from father"));
wait(NULL);
} else {
printf("this is child\n");
close(fd[1]); //关闭写入端
read(fd[0], buf, sizeof(buf)); //从管道读数据
printf("child print: %s\n", buf);
exit(1);
}
return 0;
}
程序执行结果如下:
命名管道(Named Pipes)的使用:
命名管道是一种具有持久性的管道,它以文件的形式存在于文件系统中,并允许无关进程之间进行通信。相比于匿名管道,命名管道允许不具有亲缘关系的进程之间进行通信。
创建命名管道
在Unix/Linux系统中,可以使用mkfifo()
函数创建命名管道。命名管道创建后,会在文件系统中生成一个特殊类型的文件,它可以像普通文件一样被打开、读取和写入。
使用命名管道
使用命名管道和使用普通文件一样,可以使用文件I/O操作来读取和写入数据。不同的是,命名管道的数据读取和写入是以先进先出(FIFO)的方式进行的,即写入的数据按照写入的顺序从管道中读取出来。
特点
- 命名管道是持久性的,创建后会一直存在于文件系统中,直到被显式删除。
- 允许不具有亲缘关系的进程之间进行通信。
- 数据按照写入的顺序从管道中读取出来,具有先进先出(FIFO)的特性。
示例
下面是一个简单的C语言示例,演示了如何创建和使用命名管道:
先介绍一下mkfifo:mkfifo 是用于创建命名管道(FIFO)的系统调用。在 Unix 和类 Unix 系统中,命名管道以文件的形式存在,可以用于不同进程之间进行通信。
mkfifo
函数原型
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
参数
pathname
:要创建的命名管道的路径。mode
:管道的权限,类似于open
或chmod
中使用的权限位。常用权限包括0666
(表示管道文件可读可写)。
返回值
- 成功时返回
0
。 - 失败时返回
-1
,并设置errno
以指示错误。
注意:
命名管道的读写操作是同步的,这意味着:
- 写入进程会等待直到有读取进程打开管道进行读取。
- 读取进程会等待直到有写入进程向管道写入数据。
这导致如果你先运行写入程序而没有相应的读取程序在运行,写入程序会阻塞,等待读取程序打开管道读取数据。同样地,如果你先运行读取程序而没有写入程序在运行,读取程序会阻塞,等待写入程序向管道写入数据。
为了避免这个问题,可以按以下步骤运行程序:
- 先运行读取程序
reader
,使其准备好从管道读取数据。 - 然后运行写入程序
writer
,向管道写入数据。
writer.c (写入程序)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#define FIFO_FILE "/tmp/my_fifo"
int main() {
// 创建命名管道,权限模式为 0600
if (mkfifo(FIFO_FILE, 0600) == -1) {
perror("mkfifo failed");
exit(EXIT_FAILURE);
}
// 打开命名管道以写入数据
int fd = open(FIFO_FILE, O_WRONLY);
if (fd == -1) {
perror("open failed");
exit(EXIT_FAILURE);
}
// 写入数据到命名管道
const char *message = "Hello, Named Pipe!";
write(fd, message, sizeof(message));
// 关闭文件描述符
close(fd);
return 0;
}
reader.c (读取程序)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#define FIFO_FILE "/tmp/my_fifo"
int main() {
char buffer[BUFSIZ];
// 打开命名管道以读取数据
int fd = open(FIFO_FILE, O_RDONLY);
if (fd == -1) {
perror("open failed");
exit(EXIT_FAILURE);
}
// 从命名管道读取数据
read(fd, buffer, sizeof(buffer));
printf("Received: %s\n", buffer);
// 关闭文件描述符
close(fd);
// 删除命名管道文件
unlink(FIFO_FILE);
return 0;
}
程序运行结果:
在Linux中,消息队列是进程间通信(IPC)的一种机制,它允许进程通过发送和接收消息进行通信。消息队列是系统V IPC的一部分,并且提供了一种高效且灵活的方式来在独立进程之间传递信息。
二、消息队列
消息队列的基本概念
- 消息队列:是内核维护的一个链表,存储着消息。每个消息队列都有一个唯一的标识符(队列ID)。
- 消息:每个消息包含一个类型(长整型)和一个数据部分(字节数组)。
- 消息类型:用于区分不同类型的消息,接收进程可以根据消息类型选择接收特定类型的消息。
消息队列的关键操作
消息队列的操作主要包括创建、发送、接收和控制。以下是这些操作对应的系统调用和它们的基本用法。
创建或获取消息队列
使用 msgget
函数创建或获取一个消息队列标识符。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
参数说明
key
:消息队列的键值。通过这个键值可以唯一标识一个消息队列。可以使用ftok
函数生成。msgflg
:消息队列的标志,可以是以下标志的组合:IPC_CREAT
:如果消息队列不存在,则创建一个新的消息队列。IPC_EXCL
:与IPC_CREAT
一起使用时,如果消息队列已经存在,则返回错误。- 权限标志:如
0666
,表示消息队列的访问权限。
返回值
- 成功时返回消息队列的标识符(非负整数)。
- 失败时返回
-1
,并设置errno
以指示错误类型。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
- msqid:消息队列标识符。
- msgp:指向消息结构体的指针。消息结构体的定义如下:
struct msgbuf { long mtype; // 消息类型 char mtext[1]; // 消息正文 };
- msgsz:消息正文的大小(不包括类型)。
- msgflg:操作标志(如
IPC_NOWAIT
)。
接收消息
使用 msgrcv
函数从消息队列接收消息。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
- msqid:消息队列标识符。
- msgp:指向消息结构体的指针。
- msgsz:消息正文的大小。
- msgtyp:消息类型,指定要接收的消息类型。
- msgflg:操作标志(如
IPC_NOWAIT
和MSG_NOERROR
)。
控制消息队列
使用 msgctl
函数对消息队列进行控制操作,如删除消息队列或获取消息队列的状态。
- msqid:消息队列标识符。
- cmd:控制命令(如
IPC_RMID
删除消息队列,IPC_STAT
获取消息队列状态,IPC_SET
设置消息队列属性)。 - buf:指向
msqid_ds
结构体的指针,用于存储或设置消息队列的属性。
ftok
函数
ftok
函数的原型如下:
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
- pathname:一个现有文件的路径名。通常,这个文件必须存在且可以访问。
- proj_id:一个项目标识符,是一个字符(通常是一个整数值),用于生成键值的补充标识。
ftok
的工作原理
ftok
函数通过组合 pathname
和 proj_id
生成一个唯一的键值(key_t
类型)。
为什么使用 ftok
- 唯一性:确保在同一系统中不同的IPC对象可以通过不同的键值唯一标识。
- 可重复性:只要
pathname
和proj_id
保持不变,每次调用ftok
函数生成的键值是相同的。
示例代码
以下是一个完整的示例,展示了如何创建消息队列、发送消息、接收消息并删除消息队列。
示例说明
- 进程A:发送消息到消息队列。
- 进程B:从消息队列接收消息。
进程A:发送消息
首先,编写一个程序作为发送消息的进程A。
// sender.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
struct msgbuf {
long mtype;
char mtext[100];
};
int main() {
key_t key;
int msgid;
struct msgbuf message;
// 生成唯一键
key = ftok("msgqueuefile", 65);
if (key == -1) {
perror("ftok failed");
exit(1);
}
// 创建消息队列
msgid = msgget(key, 0666 | IPC_CREAT);
if (msgid == -1) {
perror("msgget failed");
exit(1);
}
// 准备消息
message.mtype = 1; // 消息类型
strcpy(message.mtext, "Hello from Process A!");
// 发送消息
if (msgsnd(msgid, &message, sizeof(message.mtext), 0) == -1) {
perror("msgsnd failed");
exit(1);
}
printf("Message sent: %s\n", message.mtext);
return 0;
}
进程B:接收消息
然后,编写另一个程序作为接收消息的进程B。
// receiver.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/msg.h>
struct msgbuf {
long mtype;
char mtext[100];
};
int main() {
key_t key;
int msgid;
struct msgbuf message;
// 生成唯一键
key = ftok("msgqueuefile", 65);
if (key == -1) {
perror("ftok failed");
exit(1);
}
// 获取消息队列标识符
msgid = msgget(key, 0666 | IPC_CREAT);
if (msgid == -1) {
perror("msgget failed");
exit(1);
}
// 接收消息类型为 1 的消息
if (msgrcv(msgid, &message, sizeof(message.mtext), 1, 0) == -1) {
perror("msgrcv failed");
exit(1);
}
printf("Received message: %s\n", message.mtext);
// 删除消息队列(可选操作,一般在所有进程完成通信后由一个进程执行)
if (msgctl(msgid, IPC_RMID, NULL) == -1) {
perror("msgctl (IPC_RMID) failed");
exit(1);
}
return 0;
}
解释
- 生成键值:两个进程使用相同的键值生成方法(
ftok
),确保它们访问同一个消息队列。 - 创建/获取消息队列:
- 在发送进程中,消息队列不存在时创建消息队列(
IPC_CREAT
)。 - 在接收进程中,获取消息队列标识符。如果队列不存在时创建它(
IPC_CREAT
)。
- 在发送进程中,消息队列不存在时创建消息队列(
- 发送消息:发送进程将消息发送到消息队列。
- 接收消息:接收进程从消息队列中接收消息,并根据消息类型进行过滤(这里消息类型为1)。
- 删除消息队列:接收进程接收完消息后删除消息队列(
msgctl
),这是可选的,一般由一个进程在通信完成后执行。
三、共享内存
共享内存是一种高效的进程间通信(IPC)机制,它允许多个进程共享一段内存。这种方式比消息队列更高效,因为数据只需要在内存中复制一次。下面是使用共享内存的一个示例,包括创建共享内存段、附加到共享内存段、写入和读取数据,以及删除共享内存段。
比喻解释
假设你和你的朋友们在一个房间里合作完成一个项目。房间中间有一块黑板,你们都可以在上面写字、擦字和读内容。黑板就是共享内存,而你和你的朋友们就是不同的进程。
- 共享内存:黑板,所有人(进程)都可以看到和操作。
- 进程:你和你的朋友们,每个人都可以对黑板进行读写操作。
使用共享内存的步骤
- 创建或获取共享内存段。
- 将共享内存段附加到进程的地址空间。
- 访问共享内存(读写数据)。
- 分离共享内存段。
- 删除共享内存段(可选)。
共享内存函数概述
shmget
- 创建或获取一个共享内存段。shmat
- 将共享内存段附加到进程的地址空间。shmdt
- 将共享内存段从进程的地址空间分离。shmctl
- 控制共享内存段,包括删除共享内存段。
shmget
函数
int shmget(key_t key, size_t size, int shmflg);
-
参数:
key
:共享内存段的键值。通常使用ftok
生成。size
:共享内存段的大小(字节)。shmflg
:权限标志和选项。常见值包括0666
(读写权限)和IPC_CREAT
(如果共享内存段不存在则创建)。
-
返回值:
- 成功时返回共享内存段的标识符(非负整数)。
- 失败时返回
-1
,并设置errno
。
shmat
函数
void* shmat(int shmid, const void *shmaddr, int shmflg);
-
参数:
shmid
:共享内存段的标识符,由shmget
返回。shmaddr
:希望附加的地址,通常为NULL
,表示由系统决定。shmflg
:选项标志,通常为0
。
-
返回值:
- 成功时返回指向共享内存段的指针。
- 失败时返回
(void *)-1
,并设置errno
。
shmdt
函数
int shmdt(const void *shmaddr);
-
参数:
shmaddr
:共享内存段的地址,由shmat
返回。
-
返回值:
- 成功时返回
0
。 - 失败时返回
-1
,并设置errno
。
- 成功时返回
shmctl
函数
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
-
参数:
shmid
:共享内存段的标识符。cmd
:控制命令,如IPC_RMID
(删除共享内存段)、IPC_STAT
(获取共享内存段的状态)、IPC_SET
(设置共享内存段的状态)。buf
:用于存储或传递共享内存段信息的结构体,通常用于IPC_STAT
和IPC_SET
命令。
-
返回值:
- 成功时返回
0
。 - 失败时返回
-1
,并设置errno
。
- 成功时返回
代码示例
下面是一个简单的示例,展示如何使用共享内存进行进程间通信。
共享内存头文件(common.h)
#ifndef COMMON_H
#define COMMON_H
#include <sys/ipc.h>
#include <sys/shm.h>
#define SHM_SIZE 1024 // 共享内存大小
#define SHM_KEY 1234 // 共享内存键值
#endif // COMMON_H
写入共享内存的进程(writer.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "common.h"
int main() {
int shmid;
char *shmaddr;
// 创建共享内存段
shmid = shmget(SHM_KEY, SHM_SIZE, 0666 | IPC_CREAT);
if (shmid == -1) {
perror("shmget failed");
exit(1);
}
// 将共享内存段附加到进程的地址空间
shmaddr = shmat(shmid, NULL, 0);
if (shmaddr == (char *)-1) {
perror("shmat failed");
exit(1);
}
// 向共享内存写入数据
strncpy(shmaddr, "Hello from writer!", SHM_SIZE);
printf("Data written to shared memory: %s\n", shmaddr);
// 分离共享内存段
if (shmdt(shmaddr) == -1) {
perror("shmdt failed");
exit(1);
}
return 0;
}
读取共享内存的进程(reader.c)
#include <stdio.h>
#include <stdlib.h>
#include "common.h"
int main() {
int shmid;
char *shmaddr;
// 获取共享内存段
shmid = shmget(SHM_KEY, SHM_SIZE, 0666);
if (shmid == -1) {
perror("shmget failed");
exit(1);
}
// 将共享内存段附加到进程的地址空间
shmaddr = shmat(shmid, NULL, 0);
if (shmaddr == (char *)-1) {
perror("shmat failed");
exit(1);
}
// 读取共享内存中的数据
printf("Data read from shared memory: %s\n", shmaddr);
// 分离共享内存段
if (shmdt(shmaddr) == -1) {
perror("shmdt failed");
exit(1);
}
// 删除共享内存段(可选操作,一般在所有进程完成通信后由一个进程执行)
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl (IPC_RMID) failed");
exit(1);
}
return 0;
}
通过共享内存,不同进程可以非常高效地共享数据,就像大家一起在一个黑板上写字和读字一样。这种机制特别适合需要快速、大量数据交换的场景,比如多进程计算、实时数据处理等。
四、信号
在Linux中,信号是一种进程间通信(IPC)的机制,用于通知进程发生了某种事件。信号可以用于通知进程硬件异常、用户请求的中断、操作系统事件等。
常见的Linux信号
一些常见的Linux信号及其含义:
信号编号 | 信号名称 | 描述 |
1 | SIGHUP | 终端挂起或控制进程终止 |
2 | SIGINT | 来自键盘的中断(Ctrl+C) |
3 | SIGQUIT | 来自键盘的退出(Ctrl+\) |
9 | SIGKILL | 无条件终止进程(不能捕捉、阻塞或忽略) |
15 | SIGTERM | 终止进程(可以捕捉、阻塞或处理) |
17,19,23 | SIGCHLD | 子进程结束 |
18,20,22 | SIGTSTP | 来自键盘的停止(Ctrl+Z) |
19,21,23 | SIGCONT | 继续执行一个停止的进程 |
比喻解释
假设你在一个图书馆里读书(模拟一个进程正在执行任务),图书馆里有一个广播系统(相当于信号机制)。图书馆管理员(操作系统)可以通过广播系统向所有读者发送不同类型的信息(信号)。
- 警报信号(SIGINT):广播系统通知所有人立即离开图书馆(相当于按
Ctrl+C
终止进程)。 - 提示信号(SIGUSR1):广播系统通知某个读者有人找他(特定信号,应用程序可以自定义处理)。
- 关门信号(SIGTERM):广播系统通知图书馆将在10分钟后关闭,大家需要收拾东西准备离开(请求程序优雅地终止)。
- 火警信号(SIGKILL):广播系统发出紧急疏散警报,要求所有人立刻离开(强制终止进程)。
信号的处理
进程可以处理信号的方式有三种:
- 默认处理:操作系统为每个信号定义了默认的处理方式。
- 忽略信号:进程可以选择忽略某些信号。
- 自定义处理:进程可以捕捉信号并执行自定义的信号处理程序。
相关函数
signal
函数:用于指定信号的处理程序。kill
函数:用于向进程发送信号。raise
函数:用于向当前进程发送信号。alarm
函数:用于设置一个定时器,当定时器到期时发送SIGALRM
信号。pause
函数:挂起进程,直到捕获到信号。
signal
函数是用来设置一个进程的信号处理程序的。
函数原型
#include <signal.h>
void (*signal(int signum, void (*handler)(int)))(int);
参数说明
signum
:要捕捉或处理的信号编号。例如,SIGINT
是中断信号,通常是用户按Ctrl+C
触发的信号。handler
:信号处理程序,可以是以下三种之一:- 一个指向信号处理函数的指针。当信号到达时,该函数将被调用。
SIG_IGN
:表示忽略该信号。SIG_DFL
:表示使用默认的信号处理方式。
返回值
- 成功时,返回前一个信号处理函数的地址。
- 失败时,返回
SIG_ERR
,并设置errno
以指示错误。
示例代码
以下是一个示例程序,它捕捉 SIGINT
信号,并在信号到达时执行特定的处理逻辑:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
// 信号处理函数
void handle_sigint(int sig) {
printf("Caught signal %d (SIGINT). Exiting...\n", sig);
exit(0);
}
int main() {
// 设置信号处理函数
if (signal(SIGINT, handle_sigint) == SIG_ERR) {
perror("signal");
exit(1);
}
printf("Press Ctrl+C to trigger SIGINT...\n");
// 无限循环,等待信号
while (1) {
sleep(1);
}
return 0;
}
sigaction
和 signal
都是用于设置信号处理程序的函数,但 sigaction
提供了更多的功能和更高的可靠性。以下是它们的主要区别:
功能与特性
signal
函数
- 简单易用:
signal
函数的接口较简单,适用于基本的信号处理需求。 - 可移植性差:在不同的操作系统和系统版本之间,
signal
的行为可能有所不同。 - 缺乏高级特性:不支持额外的标志位和信号处理选项,无法传递额外的上下文信息。
sigaction
函数
- 高级功能:提供更多的控制选项,包括额外的标志位和信号处理选项。
- 可靠性高:在各种操作系统和系统版本之间行为一致,提供更高的可移植性。
- 丰富的信号信息:能够接收和处理更多的信号上下文信息(如信号发送者的进程ID,信号附带的值等)。
- 支持信号排队:在使用实时信号时,支持信号排队,避免信号丢失。
sigaction
函数
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
signum
:要捕捉或处理的信号编号。act
:指向包含新信号处理程序信息的struct sigaction
结构体。oldact
:指向存储旧信号处理程序信息的struct sigaction
结构体,如果不需要可以设为NULL
。
struct sigaction
结构体
struct sigaction {
void (*sa_handler)(int); // 信号处理函数指针或常量 SIG_IGN, SIG_DFL
void (*sa_sigaction)(int, siginfo_t *, void *); // 信号处理函数,用于捕捉带附加信息的信号
sigset_t sa_mask; // 在信号处理程序运行时需要屏蔽的信号集
int sa_flags; // 标志位,用于设置信号处理选项
void (*sa_restorer)(void); // 已废弃,不再使用
};
struct sigaction
是在标准库 <signal.h>
中预先声明和定义的。
字段说明
sa_handler
:指向信号处理函数的指针,也可以是SIG_IGN
(忽略信号)或SIG_DFL
(使用默认处理)。sa_sigaction
:指向带有更多信息的信号处理函数的指针,只有在设置了SA_SIGINFO
标志时才使用。sa_mask
:在处理信号时要阻塞的信号集。sa_flags
:控制信号处理的行为的标志,可以设置多个标志以启用不同的功能。sa_restorer
:已经废弃,不再使用。
常用标志
SA_SIGINFO
:使用sa_sigaction
处理函数,允许接收更多的信号信息。SA_RESTART
:使被信号中断的系统调用自动重启。SA_NOCLDWAIT
:防止子进程变为僵尸进程(用于SIGCHLD
信号)。SA_NODEFER
:不自动屏蔽正在处理的信号。
代码示例
使用 sigaction
设置信号处理程序
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
// 信号处理函数
void handle_signal(int sig, siginfo_t *info, void *ucontext) {
printf("Caught signal %d\n", sig);
if (info != NULL) {
printf("Signal originates from process %d\n", info->si_pid);
}
exit(0);
}
int main() {
struct sigaction sa;
// 设置信号处理函数
sa.sa_flags = SA_SIGINFO; // 使用 sa_sigaction 处理函数
sa.sa_sigaction = handle_signal; // 指定信号处理函数
sigemptyset(&sa.sa_mask); // 清空信号屏蔽集
// 设置 SIGINT 的处理程序
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction");
exit(EXIT_FAILURE);
}
printf("Press Ctrl+C to trigger SIGINT...\n");
// 无限循环,等待信号
while (1) {
sleep(1);
}
return 0;
}
代码解释
struct sigaction sa;
声明一个 sigaction
结构体实例 sa
,它将用于设置信号处理程序。
sa.sa_flags = SA_SIGINFO;
设置 sa_flags
成员,这个标志位决定信号处理的行为。SA_SIGINFO
标志表示使用 sa_sigaction
成员而不是 sa_handler
成员来处理信号。
sa.sa_sigaction = handle_signal;
将 sa_sigaction
成员设置为 handle_signal
函数指针,表示当捕获到信号时,调用这个信号处理函数。
sigemptyset(&sa.sa_mask);
初始化 sa_mask
成员,将其设置为空的信号集。这意味着在处理信号时,当前信号处理程序不会阻塞其他任何信号。
if (sigaction(SIGINT, &sa, NULL) == -1) { ... }
使用 sigaction
函数将 SIGINT
信号的处理程序设置为 handle_signal
函数。sigaction
函数的参数解释如下:
SIGINT
:要捕获的信号。&sa
:指向包含信号处理程序设置的sigaction
结构体。NULL
:指向一个sigaction
结构体,用于保存之前的信号处理程序。如果不需要保存之前的处理程序,可以传递NULL
。
未显式设置的成员
在 struct sigaction
结构体中,以下成员没有显式设置:
sa_handler
:因为我们设置了SA_SIGINFO
标志,所以使用sa_sigaction
而不是sa_handler
。因此,这里不需要设置sa_handler
。sa_restorer
:已经废弃,不再使用,通常忽略。sa_mask
:初始化为空信号集,这里显式地调用了sigemptyset
来进行设置。sigemptyset
是一个用于初始化信号集的函数。它将指定的信号集中的所有信号清除,使信号集为空。这在设置信号处理程序时非常有用,因为它可以确保信号集从已知的空状态开始,然后可以添加特定的信号。
假设有一个运行中的进程无法响应 SIGTERM
或其他终止信号,例如一个陷入死循环或资源争用的进程。在这种情况下,可以使用 kill -9
来强制终止它。
kill -9
是一个命令行工具,用于发送SIGKILL
信号,强制终止目标进程。SIGKILL
信号无法被捕捉、阻塞或忽略,是确保进程终止的最后手段。
示例步骤
1.查看当前运行的进程:
可以使用 ps
命令或其他进程查看工具(如 top
)来获取进程 ID。
ps -aux | grep a.out
2.使用 kill -9
终止进程:
假设我们要终止的进程 ID 是 1234
,我们可以执行以下命令:
kill -9 1234
kill
函数
kill
函数用于向指定的进程或进程组发送信号。这个函数不仅可以发送终止信号,还可以发送其他类型的信号。
#include <signal.h>
int kill(pid_t pid, int sig);
参数
pid
:要发送信号的进程ID。如果为正数,则表示单个进程的ID;如果为0,则表示发送信号给与调用进程在同一个进程组中的所有进程;如果为-1,则表示发送信号给系统中的所有进程(只有超级用户可以这样做);如果为负数,但不等于-1,则表示发送信号给进程组ID为-pid
的所有进程。sig
:要发送的信号。
返回值
- 成功时返回0。
- 失败时返回-1,并设置
errno
以指示错误。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
} else if (pid == 0) {
// 子进程
printf("Child process PID: %d\n", getpid());
while (1) {
sleep(1);
}
} else {
// 父进程
sleep(2); // 等待子进程启动
printf("Sending SIGTERM to child process...\n");
if (kill(pid, SIGTERM) == -1) {
perror("kill");
exit(EXIT_FAILURE);
}
}
return 0;
}
sigqueue
函数
sigqueue
函数用于向指定的进程发送信号,并且可以携带附加数据。这对于需要传递额外信息的信号处理程序非常有用。
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
参数
pid
:要发送信号的进程ID。sig
:要发送的信号。value
:一个联合类型sigval
,用于携带附加数据。
返回值
- 成功时返回0。
- 失败时返回-1,并设置
errno
以指示错误。
这段代码演示了如何使用信号处理函数 sigaction
和 sigqueue
来发送和处理带有附加数据的信号。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
// 信号处理函数
void handle_signal(int sig, siginfo_t *info, void *ucontext) {
if (sig == SIGUSR1) {
printf("Caught SIGUSR1 with value %d\n", info->si_value.sival_int);
}
}
int main() {
struct sigaction sa;
sa.sa_flags = SA_SIGINFO;
sa.sa_sigaction = handle_signal;
sigemptyset(&sa.sa_mask);
if (sigaction(SIGUSR1, &sa, NULL) == -1) {
perror("sigaction");
exit(EXIT_FAILURE);
}
pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
} else if (pid == 0) {
// 子进程
printf("Child process PID: %d\n", getpid());
while (1) {
sleep(1);
}
} else {
// 父进程
sleep(2); // 等待子进程启动
union sigval value;
value.sival_int = 42;
printf("Sending SIGUSR1 to child process with value %d...\n", value.sival_int);
if (sigqueue(pid, SIGUSR1, value) == -1) {
perror("sigqueue");
exit(EXIT_FAILURE);
}
}
return 0;
}
代码运行结果:
以下是对代码的详细解释:
信号处理函数
void handle_signal(int sig, siginfo_t *info, void *ucontext) {
if (sig == SIGUSR1) {
printf("Caught SIGUSR1 with value %d\n", info->si_value.sival_int);
}
}
handle_signal
是信号处理函数。- 参数:
sig
:接收到的信号编号。info
:指向siginfo_t
结构体的指针,包含了信号的附加信息。ucontext
:指向用户上下文的指针,通常在这个例子中不使用。
- 当接收到
SIGUSR1
信号时,打印附带的数据info->si_value.sival_int
。
设置信号处理程序
struct sigaction sa;
sa.sa_flags = SA_SIGINFO;
sa.sa_sigaction = handle_signal;
sigemptyset(&sa.sa_mask);
if (sigaction(SIGUSR1, &sa, NULL) == -1) {
perror("sigaction");
exit(EXIT_FAILURE);
}
struct sigaction sa
:声明sigaction
结构体变量sa
。sa.sa_flags = SA_SIGINFO
:设置SA_SIGINFO
标志,使用sa_sigaction
成员而不是sa_handler
。sa.sa_sigaction = handle_signal
:指定信号处理函数handle_signal
。sigemptyset(&sa.sa_mask)
:初始化信号屏蔽集,清空信号集。sigaction(SIGUSR1, &sa, NULL)
:设置SIGUSR1
的处理程序。如果调用失败,打印错误信息并退出程序。
创建子进程
pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
} else if (pid == 0) {
// 子进程
printf("Child process PID: %d\n", getpid());
while (1) {
sleep(1);
}
} else {
// 父进程
sleep(2); // 等待子进程启动
union sigval value;
value.sival_int = 42;
printf("Sending SIGUSR1 to child process with value %d...\n", value.sival_int);
if (sigqueue(pid, SIGUSR1, value) == -1) {
perror("sigqueue");
exit(EXIT_FAILURE);
}
}
pid_t pid = fork()
:创建子进程。- 如果
fork
失败,打印错误信息并退出程序。 - 如果
pid == 0
,这是在子进程中执行的代码:- 打印子进程的 PID。
- 进入一个无限循环,每秒休眠一次。
- 如果
pid > 0
,这是在父进程中执行的代码:- 父进程等待 2 秒以确保子进程启动。
- 创建一个
union sigval
变量value
并设置value.sival_int = 42
。 - 打印发送信号的信息。
- 使用
sigqueue
向子进程发送SIGUSR1
信号,并附带整数值42
。如果发送失败,打印错误信息并退出程序。
- 如果
信号发送和接收
- 发送信号:父进程使用
sigqueue
发送SIGUSR1
信号给子进程,并携带一个整数值42
。 - 接收信号:子进程在接收到
SIGUSR1
信号时,通过handle_signal
函数处理,并打印接收到的信号值42
。
siginfo_t
结构体
siginfo_t
的定义在 <signal.h>
头文件中。它的结构大致如下:
typedef struct siginfo {
int si_signo; // 信号编号
int si_errno; // 错误码(如果有的话)
int si_code; // 信号的来源
pid_t si_pid; // 发送信号的进程ID
uid_t si_uid; // 发送信号的用户ID
void *si_addr; // 相关地址(取决于信号)
int si_status; // 子进程状态(对于 SIGCHLD 信号)
int si_band; // 信号带(对于 SIGPOLL 信号)
union sigval si_value; // 信号附带的值(对于实时信号)
} siginfo_t;
union sigval
是一个联合体,可以容纳不同类型的数据(整数和指针),使得信号处理函数可以灵活处理不同类型的附加数据。
总结
union sigval
是一个联合体,可以存储一个整数或一个指针,用于通过信号发送附加数据。- 在发送信号时,通过
sigqueue
函数将union sigval
变量作为信号的一部分发送给目标进程。 - 在信号处理函数中,通过
siginfo_t
结构体访问union sigval
中的附加数据。
这段代码通过 sigqueue
向子进程发送 SIGUSR1
信号,并附带一个整数值 42
。子进程接收到信号后,在信号处理函数中打印该整数值。
五、信号量
3个API:
semget、semctl、semop
1、semget
函数用于创建一个新的信号量集或获取一个现有的信号量集。它的函数原型如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
参数
key
:信号量集的键值,用于标识信号量集。可以使用ftok
函数生成。nsems
:信号量集中的信号量数量。如果是获取现有的信号量集,该值可以为 0。semflg
:标志参数,用于指定创建方式和访问权限,例如IPC_CREAT
和0666
(读写权限)。
返回值
成功时返回信号量集的标识符(非负整数),失败时返回 -1,并设置 errno
。
示例代码
以下是一个简单的示例代码,展示如何使用 semget
函数来创建一个新的信号量集:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <errno.h>
int main() {
// 使用 ftok 生成唯一的键值
key_t key = ftok("semfile", 'A');
if (key == -1) {
perror("ftok");
exit(EXIT_FAILURE);
}
// 创建一个新的信号量集,包含 1 个信号量
int semid = semget(key, 1, 0666 | IPC_CREAT);
if (semid == -1) {
perror("semget");
exit(EXIT_FAILURE);
}
printf("Semaphore ID: %d\n", semid);
return 0;
}
创建信号量集
int semid = semget(key, 1, 0666 | IPC_CREAT);
if (semid == -1) {
perror("semget");
exit(EXIT_FAILURE);
}
使用 semget
函数创建一个新的信号量集,包含 1 个信号量。0666
是信号量集的访问权限(读写权限),IPC_CREAT
标志表示如果信号量集不存在则创建它。如果调用成功,返回信号量集的标识符(非负整数);如果失败,返回 -1,并设置 errno
。
2、semctl
函数是用于控制信号量集或信号量的操作函数。它可以用于初始化信号量的值、获取信号量的值、删除信号量集等操作。semctl
函数的原型如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
参数
semid
:信号量集的标识符,由semget
返回。semnum
:信号量集中的信号量编号(索引)。cmd
:控制命令,定义了要执行的操作。- 变长参数:取决于
cmd
的类型,可以是一个整数值或一个指向union semun
的指针。
常用控制命令
SETVAL
:设置单个信号量的值。GETVAL
:获取单个信号量的值。IPC_RMID
:删除信号量集。SETALL
:设置信号量集中所有信号量的值。GETALL
:获取信号量集中所有信号量的值。
union semun
为了使用 semctl
的一些控制命令,需要定义一个 union semun
。虽然 POSIX 标准中并没有在头文件中定义该联合体,但实际使用时需要手动定义:
union semun {
int val; // 值用于 SETVAL
struct semid_ds *buf; // 缓冲区用于 IPC_STAT, IPC_SET
unsigned short *array; // 数组用于 GETALL, SETALL
};
示例代码
下面展示如何使用 semctl
来初始化信号量、获取信号量值以及删除信号量集。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <errno.h>
// 定义 union semun
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
int main() {
// 使用 ftok 生成唯一的键值
key_t key = ftok("semfile", 'A');
if (key == -1) {
perror("ftok");
exit(EXIT_FAILURE);
}
// 创建一个新的信号量集,包含 1 个信号量
int semid = semget(key, 1, 0666 | IPC_CREAT);
if (semid == -1) {
perror("semget");
exit(EXIT_FAILURE);
}
// 初始化信号量
union semun sem_union;
sem_union.val = 1; // 设置信号量的初始值
if (semctl(semid, 0, SETVAL, sem_union) == -1) {
perror("semctl SETVAL");
exit(EXIT_FAILURE);
}
// 获取信号量的值
int sem_value = semctl(semid, 0, GETVAL);
if (sem_value == -1) {
perror("semctl GETVAL");
exit(EXIT_FAILURE);
}
printf("Semaphore value: %d\n", sem_value);
// 删除信号量集
if (semctl(semid, 0, IPC_RMID) == -1) {
perror("semctl IPC_RMID");
exit(EXIT_FAILURE);
}
// 再次尝试获取信号量集,检查是否删除成功
semid = semget(key, 1, 0666);
if (semid == -1) {
if (errno == ENOENT) {
printf("Semaphore set successfully deleted.\n");
} else {
perror("semget after IPC_RMID");
exit(EXIT_FAILURE);
}
} else {
printf("Semaphore set still exists with ID: %d\n", semid);
}
return 0;
}
3、在 Linux 中,semop
函数是用于对信号量集进行操作的系统调用。它的主要作用是对信号量进行 P(等待)操作和 V(释放)操作,从而实现进程间的同步与互斥。semop
的定义如下:
semop
函数原型
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nsops);
参数说明
semid
:信号量集的标识符,由semget
函数返回。sops
:指向struct sembuf
结构数组的指针,该数组描述了要执行的信号量操作。nsops
:sops
数组中的操作数量。(有几个信号量,就是几)
struct sembuf
结构
struct sembuf
结构定义了对单个信号量的操作:
struct sembuf {
unsigned short sem_num; // 信号量集中的信号量编号(索引)
short sem_op; // 要执行的操作
short sem_flg; // 操作标志
};
sem_op
值
sem_op > 0
:释放信号量,增加信号量的值。sem_op < 0
:等待信号量,减少信号量的值。如果信号量的值小于操作的绝对值,则进程会阻塞,直到信号量的值足够大。sem_op == 0
:等待信号量变为零。
sem_flg
标志
IPC_NOWAIT
:如果操作无法立即执行,则不阻塞进程,立即返回错误。SEM_UNDO
:如果进程在操作信号量后终止,系统会自动撤销操作。
示例代码
下面是一个使用 semop
函数进行 P 操作和 V 操作的示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
#include <errno.h>
// 定义 union semun
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
void sem_op(int semid, int sem_num, int sem_op);
int main() {
// 使用 ftok 生成唯一的键值
key_t key = ftok("semfile", 'A');
if (key == -1) {
perror("ftok");
exit(EXIT_FAILURE);
}
// 创建一个新的信号量集,包含 1 个信号量
int semid = semget(key, 1, 0666 | IPC_CREAT);
if (semid == -1) {
perror("semget");
exit(EXIT_FAILURE);
}
// 初始化信号量
union semun sem_union;
sem_union.val = 1; // 设置信号量的初始值
if (semctl(semid, 0, SETVAL, sem_union) == -1) {
perror("semctl SETVAL");
exit(EXIT_FAILURE);
}
// Fork 一个子进程
pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
} else if (pid == 0) {
// 子进程
printf("Child process trying to decrease semaphore...\n");
sem_op(semid, 0, -1); // P 操作
printf("Child process successfully decreased semaphore. Critical section.\n");
sleep(2); // 模拟在临界区的工作
sem_op(semid, 0, 1); // V 操作
printf("Child process released semaphore.\n");
} else {
// 父进程
printf("Parent process trying to decrease semaphore...\n");
sem_op(semid, 0, -1); // P 操作
printf("Parent process successfully decreased semaphore. Critical section.\n");
sleep(2); // 模拟在临界区的工作
sem_op(semid, 0, 1); // V 操作
printf("Parent process released semaphore.\n");
// 等待子进程结束
wait(NULL);
// 删除信号量集
if (semctl(semid, 0, IPC_RMID) == -1) {
perror("semctl IPC_RMID");
exit(EXIT_FAILURE);
}
}
return 0;
}
void sem_op(int semid, int sem_num, int sem_op) {
struct sembuf sops;
sops.sem_num = sem_num;
sops.sem_op = sem_op;
sops.sem_flg = 0;
if (semop(semid, &sops, 1) == -1) {
perror("semop");
exit(EXIT_FAILURE);
}
}
代码解释
-
生成键值和创建信号量集
key_t key = ftok("semfile", 'A'); int semid = semget(key, 1, 0666 | IPC_CREAT);
使用
ftok
生成键值,并使用semget
创建一个信号量集。 -
初始化信号量
union semun sem_union; sem_union.val = 1; if (semctl(semid, 0, SETVAL, sem_union) == -1) { perror("semctl SETVAL"); exit(EXIT_FAILURE); }
使用
semctl
将信号量的初始值设为 1。 -
定义 P 操作和 V 操作
void sem_op(int semid, int sem_num, int sem_op) { struct sembuf sops; sops.sem_num = sem_num; sops.sem_op = sem_op; sops.sem_flg = 0; if (semop(semid, &sops, 1) == -1) { perror("semop"); exit(EXIT_FAILURE); } }
该函数用于执行 P 操作和 V 操作。参数
sem_op
为负值时表示 P 操作,为正值时表示 V 操作。 -
父子进程的同步
pid_t pid = fork(); if (pid == -1) { perror("fork"); exit(EXIT_FAILURE); } else if (pid == 0) { // 子进程 printf("Child process trying to decrease semaphore...\n"); sem_op(semid, 0, -1); // P 操作 printf("Child process successfully decreased semaphore. Critical section.\n"); sleep(2); // 模拟在临界区的工作 sem_op(semid, 0, 1); // V 操作 printf("Child process released semaphore.\n"); } else { // 父进程 printf("Parent process trying to decrease semaphore...\n"); sem_op(semid, 0, -1); // P 操作 printf("Parent process successfully decreased semaphore. Critical section.\n"); sleep(2); // 模拟在临界区的工作 sem_op(semid, 0, 1); // V 操作 printf("Parent process released semaphore.\n"); // 等待子进程结束 wait(NULL); // 删除信号量集 if (semctl(semid, 0, IPC_RMID) == -1) { perror("semctl IPC_RMID"); exit(EXIT_FAILURE); } }
通过
fork
创建子进程,然后父子进程分别执行 P 操作进入临界区,执行完临界区任务后执行 V 操作释放信号量。最后,父进程删除信号量集。