一、 进程间通信
常见进程间通信方式包括管道,命名管道,共享内存,信号量,消息队列,套接字和信号七种。
1. 管道
管道(Pipe):单向通信,只能在具有亲缘关系的进程之间使用。
头文件
#include <unistd.h>
#include <stdlib.h>
使用管道需要经过以下步骤:
- 定义一个int类型的数组来存储管道的读写端,例如:int fd[2]。
- 调用pipe函数创建管道:int pipe(int fd[2])。
- 在父进程中,关闭管道的读端或写端,然后向管道写入数据:write(fd[1], buffer, size)。
- 在子进程中,关闭另一端的管道,然后从管道读取数据:read(fd[0], buffer, size)。
- 不使用管道时关闭读写端:close(fd[0])和close(fd[1])
注意事项:
-
管道是单向的,因此需要分别打开读写端。
-
如果写入的数据超出了管道的缓冲区大小,将会被截断。
-
使用管道时,要保证读写顺序正确,否则可能会导致死锁等问题。
下面是一个简单的 C 语言管道使用示例代码:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> int main() { int pipefd[2]; pid_t pid; if (pipe(pipefd) == -1) { perror("pipe"); exit(EXIT_FAILURE); } pid = fork(); if (pid == -1) { perror("fork"); exit(EXIT_FAILURE); } if (pid == 0) { /* Child process */ close(pipefd[1]); /* Close unused write end */ char buf; while (read(pipefd[0], &buf, 1) > 0) write(STDOUT_FILENO, &buf, 1); write(STDOUT_FILENO, "\n", 1); close(pipefd[0]); _exit(EXIT_SUCCESS); } else { /* Parent process */ close(pipefd[0]); /* Close unused read end */ const char *msg = "Hello, world!"; write(pipefd[1], msg, strlen(msg)); close(pipefd[1]); /* Reader will see EOF */ wait(NULL); /* Wait for child */ exit(EXIT_SUCCESS); } }
这个程序创建了一个管道,然后创建了一个子进程,子进程从管道读取数据并在标准输出上打印出来,父进程向管道写入一条消息。当父进程完成写入后,它关闭管道的写端,这会导致子进程结束循环并退出。父进程等待子进程结束后也退出。
2. 命名管道
命名管道(Named Pipe):允许无亲缘关系的进程间进行通信。
使用方法
-
使用mkfifo函数创建一个命名管道文件
mkfifo("/tmp/myfifo", 0666);
-
打开命名管道文件,可以使用open函数
int fd = open("/tmp/myfifo", O_WRONLY);
-
写入数据到命名管道,可以使用write函数
char *msg = "Hello, world!"; write(fd, msg, strlen(msg)+1);
-
关闭命名管道文件,可以使用close函数
close(fd);
-
在另一个进程中读取命名管道中的数据,也需要打开命名管道文件,可以使用open函数
int fd = open("/tmp/myfifo", O_RDONLY);
-
从命名管道中读取数据,可以使用read函数
char buf[1024]; read(fd, buf, sizeof(buf)); printf("Received message: %s\n", buf);
-
关闭命名管道文件,可以使用close函数
close(fd);
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#define FIFO_NAME "/tmp/myfifo"
int main() {
int fd;
char buf[256];
mkfifo(FIFO_NAME, 0666); // 创建命名管道
printf("Waiting for a writer...\n");
fd = open(FIFO_NAME, O_RDONLY); // 打开命名管道并等待写入者
printf("Received data: ");
while (read(fd, buf, sizeof(buf)) > 0) { // 从命名管道中读取数据
printf("%s", buf);
}
printf("\n");
close(fd);
unlink(FIFO_NAME); // 关闭并删除命名管道
return 0;
}
3. 共享内存
共享内存(Shared Memory):多个进程可以访问同一块物理内存。
使用方法
- 创建共享内存区域:使用shmget函数创建一个共享内存区域,并返回一个唯一的标识符。
- 连接共享内存区域:使用shmat函数将进程连接到共享内存区域。该函数返回一个指向共享内存区域的指针,可以通过该指针访问共享内存区域。
- 访问共享内存:通过指针访问共享内存区域,可以读取和修改共享内存中的数据。
- 分离共享内存区域:使用shmdt函数将进程与共享内存区域分离。
- 删除共享内存区域:使用shmctl函数删除共享内存区域。
示例代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define SHM_SIZE 1024
int main() {
int shmid;
key_t key;
char *shm, *s;
// 创建共享内存段
key = 1234;
if ((shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666)) < 0) {
perror("shmget");
exit(1);
}
// 将共享内存连接到进程地址空间
if ((shm = shmat(shmid, NULL, 0)) == (char *) -1) {
perror("shmat");
exit(1);
}
// 在共享内存中写入数据
strncpy(shm, "Hello, World!", SHM_SIZE);
// 读取共享内存中的数据
for (s = shm; *s != '\0'; s++) {
putchar(*s);
}
putchar('\n');
// 解除与进程的连接
shmdt(shm);
// 删除共享内存段
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
注意事项
在使用共享内存时需要注意同步问题,避免多个进程同时修改共享内存数据导致冲突。可以使用信号量或互斥锁等机制来解决同步问题。
4. 信号量
信号量(Semaphore):用于控制对共享资源的访问,防止出现竞态条件。
使用方法
-
包含头文件:#include <semaphore.h>
-
定义信号量变量:sem_t sem;
-
初始化信号量:sem_init(&sem, 0, init_value);,其中init_value为信号量初值。
-
使用信号量进行同步操作:
-
等待信号量:sem_wait(&sem);,如果信号量的值大于0,则将信号量的值减1;否则阻塞等待。
-
发送信号量:sem_post(&sem);,将信号量的值加1。
-
-
销毁信号量:sem_destroy(&sem);
示例代码
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
sem_t sem;
void* thread_func(void* arg) {
sem_wait(&sem); // 等待信号量变为非零值
printf("Thread %ld is running!\n", pthread_self());
sem_post(&sem); // 增加信号量值
return NULL;
}
int main() {
int i;
pthread_t tid[5];
// 初始化信号量
sem_init(&sem, 0, 1);
for (i = 0; i < 5; i++) {
pthread_create(&tid[i], NULL, &thread_func, NULL);
}
// 延迟主线程执行,以保证子线程已经启动并等待信号量
sleep(1);
sem_post(&sem); // 增加信号量值
for (i = 0; i < 5; i++) {
pthread_join(tid[i], NULL);
}
// 销毁信号量
sem_destroy(&sem);
return 0;
}
注意事项
- 信号量只能在进程内共享,不能跨进程共享。
- 信号量的初始值应该正确设置,避免出现死锁或饥饿的情况。
- 在使用信号量时应当保证原子性,避免多个进程同时操作信号量导致竞态条件的发生。
5. 消息队列
消息队列(Message Queue):消息发送者将消息发送到队列中,接收者从队列中取出消息。
使用方法
-
包含头文件:
#include <sys/msg.h>
-
定义消息结构体,例如:
struct msgbuf { long mtype; // 消息类型 char mtext[1024]; // 消息内容 };
-
调用msgget函数创建一个消息队列,例如:
int msgid = msgget(key, IPC_CREAT|0666);
-
使用msgsnd函数往消息队列发送消息,例如:
struct msgbuf message; message.mtype = 1; strcpy(message.mtext, "Hello World"); msgsnd(msgid, &message, sizeof(message.mtext), 0);
-
使用msgrcv函数从消息队列接收消息,例如:
struct msgbuf message; msgrcv(msgid, &message, sizeof(message.mtext), 1, 0); printf("%s\n", message.mtext);
-
使用msgctl函数删除消息队列,例如:
msgctl(msgid, IPC_RMID, 0);
示例代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#define MSG_SIZE 1024
struct msgbuf {
long mtype;
char mtext[MSG_SIZE];
};
int main() {
key_t key;
int msgid;
struct msgbuf message;
// 创建或获取一个消息队列
key = ftok("msgqtest", 'b');
msgid = msgget(key, 0666 | IPC_CREAT);
if (msgid == -1) {
perror("msgget");
exit(1);
}
printf("Message Queue ID: %d\n", msgid);
// 发送一条消息到消息队列
message.mtype = 1;
sprintf(message.mtext, "Hello, Message Queue!");
msgsnd(msgid, &message, sizeof(message), 0);
// 接收并打印队列中的消息
msgrcv(msgid, &message, sizeof(message), 0, 0);
printf("Received Message: %s\n", message.mtext);
// 删除消息队列
msgctl(msgid, IPC_RMID, NULL);
return 0;
}
注意事项
- 消息格式应该简单、清晰,并且易于处理。消息内容应该只包含必要的信息。
- 在选择消息队列软件时,需要考虑其性能、可靠性、扩展性和管理性等方面。
- 使用适当的队列长度限制来避免内存溢出问题。
- 处理消息时要确保线程安全,防止多个线程同时访问同一条消息。
- 对于高并发场景,可以使用分布式消息队列来提高性能。
- 注意消息队列中的消息顺序,确保按照正确的顺序进行处理。
- 当消息队列出现故障时,需要有相应的容错机制。
- 需要定期监控消息队列的状态,及时发现并解决潜在的问题。
6. 套接字
套接字(Socket):适用于不同计算机或操作系统之间的进程通信。
使用方法
-
创建socket:调用socket函数,指定协议族、类型和协议参数,创建一个套接字描述符。
-
绑定socket:调用bind函数,将套接字描述符绑定到本地IP地址和端口号上。
-
监听socket(可选):调用listen函数,将套接字设置为监听状态,等待客户端发起连接请求。
-
接受连接(如果有客户端连接):调用accept函数,等待客户端连接请求,并返回一个新的套接字描述符,用于与该客户端进行通信。
-
进行通信:使用recv函数从套接字中接收数据,使用send函数向套接字发送数据。
-
关闭套接字:使用close函数关闭套接字。
示例代码
服务器端
// server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main()
{
// 1. 创建监听的套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1)
{
perror("socket");
exit(0);
}
// 2. 将socket()返回值和本地的IP端口绑定到一起
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(10000); // 大端端口
// INADDR_ANY代表本机的所有IP, 假设有三个网卡就有三个IP地址
// 这个宏可以代表任意一个IP地址
// 这个宏一般用于本地的绑定操作
addr.sin_addr.s_addr = INADDR_ANY; // 这个宏的值为0 == 0.0.0.0
// inet_pton(AF_INET, "192.168.237.131", &addr.sin_addr.s_addr);
int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret == -1)
{
perror("bind");
exit(0);
}
// 3. 设置监听
ret = listen(lfd, 128);
if(ret == -1)
{
perror("listen");
exit(0);
}
// 4. 阻塞等待并接受客户端连接
struct sockaddr_in cliaddr;
int clilen = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &clilen);
if(cfd == -1)
{
perror("accept");
exit(0);
}
// 打印客户端的地址信息
char ip[24] = {0};
printf("客户端的IP地址: %s, 端口: %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, sizeof(ip)),
ntohs(cliaddr.sin_port));
// 5. 和客户端通信
while(1)
{
// 接收数据
char buf[1024];
memset(buf, 0, sizeof(buf));
int len = read(cfd, buf, sizeof(buf));
if(len > 0)
{
printf("客户端say: %s\n", buf);
write(cfd, buf, len);
}
else if(len == 0)
{
printf("客户端断开了连接...\n");
break;
}
else
{
perror("read");
break;
}
}
close(cfd);
close(lfd);
return 0;
}
客户端
// client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main()
{
// 1. 创建通信的套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1)
{
perror("socket");
exit(0);
}
// 2. 连接服务器
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(10000); // 大端端口
inet_pton(AF_INET, "192.168.237.131", &addr.sin_addr.s_addr);
int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr));
if(ret == -1)
{
perror("connect");
exit(0);
}
// 3. 和服务器端通信
int number = 0;
while(1)
{
// 发送数据
char buf[1024];
sprintf(buf, "你好, 服务器...%d\n", number++);
write(fd, buf, strlen(buf)+1);
// 接收数据
memset(buf, 0, sizeof(buf));
int len = read(fd, buf, sizeof(buf));
if(len > 0)
{
printf("服务器say: %s\n", buf);
}
else if(len == 0)
{
printf("服务器断开了连接...\n");
break;
}
else
{
perror("read");
break;
}
sleep(1); // 每隔1s发送一条数据
}
close(fd);
return 0;
}
注意事项
- 确保正确设置套接字类型,如TCP或UDP。
- 在使用套接字前,必须先创建套接字并绑定地址。
- 使用recv()和send()等函数时,要确保正确处理返回值,以防止数据丢失或错误。
- 对于长时间运行的套接字连接,应该设置超时以避免挂起。
- 在使用多线程时,确保在共享资源上进行同步以避免竞争条件。
- 避免使用过多的套接字,以防止系统资源耗尽。
- 了解不同操作系统和网络环境之间可能存在的差异,以便编写可移植的代码。
- 在开发网络应用程序时,始终考虑安全性和鲁棒性。
7.信号
信号(Signal):进程会收到一个信号,可以用来通知进程发生了某个事件。
使用方法
- 定义一个信号处理函数,用于在收到特定信号时执行操作。
- 注册信号处理函数,以便在收到信号时调用它。
- 发送信号,可使用系统函数如 kill() 或 raise()。
- 等待信号,可以使用系统函数如 pause() 或 sigsuspend()。
示例代码
#include <stdio.h>
#include <signal.h>
void sigint_handler(int signo) {
printf("Received SIGINT signal.\n");
}
int main() {
signal(SIGINT, sigint_handler);
printf("Waiting for a SIGINT signal...\n");
while(1) {
// do nothing and wait for signal
}
return 0;
}
注意事项
二、 线程间通信
线程间常用的通信方式有互斥锁,读写锁,条件变量,自旋锁和屏障
1. 互斥锁(Mutex)
控制对共享资源的访问,确保同一时刻只有一个线程可以访问该资源。
2. 读写锁(Read-Write Lock)
允许多个线程同时读取共享资源,但在写入时必须互斥。
3. 条件变量(Condition Variable)
让线程等待某些条件满足后再继续执行,用于线程间通信和同步。
4. 自旋锁(Spin Lock)
在使用短时间内的锁保护共享资源时,相比于互斥锁,自旋锁不会导致线程阻塞,而是一直进行忙等待直到获得锁。
5. 屏障(Barrier)
让多个线程在特定点处等待彼此,直到所有线程都到达这个点才能继续执行后面的代码。