进程通信是指在不同进程之间传输数据或交换信息的过程。由于进程各自拥有独立的内存地址空间,它们之间的数据不能直接通过内存地址来访问,因此需要通过特定的通信机制来实现信息交换。
进程通信的必要性
资源共享:多个进程可能需要访问共享资源,如共享内存、文件等,进程通信是实现资源共享的关键。
协同工作:多个进程可能需要协同完成某项任务,进程间的通信是协调它们工作的重要手段。
状态同步:一个进程的状态变化可能需要通知其他进程,以便它们作出相应的调整。
Linux 中进程间通信方式
只举例管道、消息队列等代码。同时不推荐进行双向操作(双工),因为比较古老,容易出现问题,比如出现混乱,一方发送的消息自己也接收。如果要实现全双工,可以使用两条消息队列(管道)分别负责两个方向的通信。
1. 管道(Pipes)
匿名管道(Anonymous Pipes):仅存在于内存中的管道,用于具有亲缘关系的进程间通信(如父子进程)。它是一种半双工通信方式,数据只能单向流动。通过pipe()系统调用创建。
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char const *argv[])
{
/*
0 : 读
1 : 写
*/
int pipefd[2];
// 将程序传递进来的第一个命令行参数 通过管道传输给子进程
if (argc != 2)
{
fprintf(stderr, "%s请填写需要传递的信息\n", argv[0]);
exit(EXIT_FAILURE);
}
// 创建管道
if(pipe(pipefd) == -1)
{
perror("创建管道失败!\n");
exit(EXIT_FAILURE);
}
// 复制父子进程
pid_t cpid = fork();
if (cpid == -1)
{
perror("fork");
exit(EXIT_FAILURE);
} else if (cpid == 0)
{
// 子进程:读取管道的数据,打印到控制台
close(pipefd[1]);
char str[100] = {0};
sprintf(str, "子进程(%d)接收信息\n", getpid());
write(STDOUT_FILENO, str, sizeof(str));
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
{
// 父进程:写入管道数据,提供给子进程读
// 单向通信,一个写,一个读 => 先关闭读,再写
close(pipefd[0]);
// 将数据写入到管道中
printf("父进程(%d)向子进程传递信息\n", getpid());
write(pipefd[1], argv[1], strlen(argv[1]));
close(pipefd[1]);
waitpid(cpid, NULL, 0);
exit(EXIT_SUCCESS);
}
return 0;
}
命名管道(Named Pipes或FIFOs):也称为FIFO(First In First Out),它在文件系统中以文件的形式存在,因此允许任意进程间进行通信。通过mkfifo()或mknod()系统调用创建。命名管道具有持久性,直到被显式删除。代码如下,这里为收发两端的代码,需要打开两个控制台进行测试。
fifo_read.c
#include <fcntl.h>
#include <errno.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define PIPO_PATH ("/tmp/myfifo")
int main(int argc, char const *argv[])
{
if (mkfifo(PIPO_PATH, 0664))
{
perror("mkfifo");
if (errno != EEXIST) // 文件已存在
{
exit(EXIT_FAILURE);
}
}
// 对有名管道的特殊文件 创建fd
int fd = open(PIPO_PATH, O_RDONLY);
if (fd == -1)
{
perror("open");
exit(EXIT_FAILURE);
}
char buf[100];
ssize_t read_num;
// 读取管道信息写入到控制台
while((read_num = read(fd, buf, 100)) > 0)
{
write(STDOUT_FILENO, buf, read_num);
}
if(read_num < 0)
{
perror("read");
close(fd);
exit(EXIT_FAILURE);
}
printf("接受管道信息完成 -->> 进程终止\n");
close(fd);
// 释放管道:清除对应的特殊文件
if(unlink(PIPO_PATH))
{
perror("unlink");
exit(EXIT_FAILURE);
}
return 0;
}
fifo_write.c
#include <fcntl.h>
#include <errno.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define PIPO_PATH ("/tmp/myfifo")
int main(int argc, char const *argv[])
{
if (mkfifo(PIPO_PATH, 0664))
{
perror("mkfifo");
if (errno != EEXIST) // 文件已存在
{
exit(EXIT_FAILURE);
}
}
// 对有名管道的特殊文件 创建fd
// 创建完成之后,后续是可以重复使用的,不推荐使用,推荐每次用完释放
int fd = open(PIPO_PATH, O_WRONLY);
if (fd == -1)
{
perror("open");
exit(EXIT_FAILURE);
}
char buf[100];
ssize_t read_num;
// 读取控制台数据写入到管道中
while((read_num = read(STDIN_FILENO, buf, 100)) > 0)
{
write(fd, buf, read_num);
}
if(read_num < 0)
{
perror("read");
close(fd);
exit(EXIT_FAILURE);
}
printf("发送数据到管道完成 -->> 进程终止\n");
close(fd);
// 释放管道:清除对应的特殊文件
if(unlink(PIPO_PATH))
{
perror("unlink");
exit(EXIT_FAILURE);
}
return 0;
}
2. 信号(Signals)
信号是软件层次上对中断机制的一种模拟,用于通知进程某个事件已经发生。进程可以选择忽略信号、捕捉信号并执行特定的信号处理函数,或者按默认方式处理信号。信号主要用于异步通知,不适用于数据传输。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void sigint_handler(int signum)
{
printf("\nCOPY THAT __ %d __ SIGNAL : END\n", signum);
exit(signum);
}
int main(int argc, char const *argv[])
{
if (signal(SIGINT, sigint_handler) == SIG_ERR)
{
perror("signal");
return 1;
}
while (1)
{
sleep(1);
printf("Hello, World!\n");
}
return 0;
}
3. 消息队列(Message Queues)
消息队列是消息的链表,存放在内核中并由消息队列标识符标识。它允许一个或多个进程向它写入与读取消息。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
#include <fcntl.h>
#include <sys/stat.h>
#include <mqueue.h>
#include <stdio.h>
#include <time.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
// 创建消息队列
struct mq_attr attr;
// 有用的参数 表示消息队列的容量
attr.mq_maxmsg = 10;
attr.mq_msgsize = 100;
// 被忽略的消息 在创建消息的时候用不到
attr.mq_flags = 0;
attr.mq_curmsgs = 0;
char * mq_name = "/parent_child_mq";
mqd_t mqdes = mq_open(mq_name, O_RDWR | O_CREAT, 0664, &attr);
if (mqdes == (mqd_t) -1)
{
perror("mq_open");
exit(EXIT_FAILURE);
}
// 创建父子进程
pid_t pid = fork();
if (pid < 0)
{
perror("fork");
exit(EXIT_FAILURE);
} else if (pid == 0)
{
// 子进程 等待接收消息队列中的信息
char read_buf[100];
struct timespec time_info;
for (size_t i = 0; i < 10; i++)
{
// 清空接收数据的缓冲区
memset(read_buf, 0, 100);
// 设置接收数据的等待时间
clock_gettime(0, &time_info);
time_info.tv_sec += 15; // 等待 15 s
// 接受消息队列的数据 打印到控制台
if (mq_timedreceive(mqdes, read_buf, 100, NULL, &time_info) == -1)
{
perror("mq_timedreceive");
}
printf("子进程接收到数据:%s\n", read_buf);
}
} else
{
// 父进程 发送消息到消息队列中
char send_buf[100];
struct timespec time_info;
for (size_t i = 0; i < 10; i++)
{
// 清空处理buf
memset(send_buf, 0, 100);
sprintf(send_buf, "父进程的第%ld次发送消息\n", i + 1);
// 获取当前的具体时间 clock_gettime(CLOCK_REALTIME, &time_info);
clock_gettime(0, &time_info);
time_info.tv_sec += 5; // 等待 5 s
// 发送消息
if (mq_timedsend(mqdes, send_buf, strlen(send_buf), 0, &time_info) == -1)
{
perror("mq_timedsend");
}
printf("父进程发送一条消息,休眠 1 s\n");
sleep(1);
}
}
// 最终不管是父进程还是子进程都需要释放消息队列的引用
close(mqdes);
// 清除消息队列只需要执行一次
if (pid > 0)
{
mq_unlink(mq_name);
}
return 0;
}
4. 共享内存(Shared Memory)
共享内存允许多个进程访问同一块内存区域。这种方式通常比较高效,因为数据不需要在进程间复制。但是,使用共享内存需要处理进程间的同步和互斥问题,以防止数据不一致。
#include <sys/mman.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#define SHM_SIZE 1024
int main(int argc, char const *argv[])
{
char * share;
int statu_loc;
// 1. 创建一个共享内存对象
char shm_name[BUFSIZ] = {0};
sprintf(shm_name, "/letter%d", getpid());
int fd = shm_open(shm_name, O_RDWR | O_CREAT, 0644);
if (fd < 0)
{
perror("shm_open");
exit(EXIT_FAILURE);
}
// 2. 设置共享内存对象大小
ftruncate(fd, SHM_SIZE);
// 3. 内存映射
share = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (share == MAP_FAILED)
{
perror("mmap");
exit(EXIT_FAILURE);
}
// 映射完成,关闭fd连接 不是释放
close(fd);
// 4. 使用内存映射实现进程间的通讯
pid_t pid = fork();
if (pid < 0)
{
perror("fork");
exit(EXIT_FAILURE);
} else if (pid == 0)
{
// 子进程
strcpy(share, "This is a test!\n");
printf("子进程__%d__完成发送\n", getpid());
} else
{
// 父进程
waitpid(pid, &statu_loc, 0); // 状态可以为 NULL
printf("父进程__%d__接收子进程__%d__:%s\n", getpid(), pid, share);
// 5. 释放映射区
int res_munmap = munmap(share, SHM_SIZE);
if (res_munmap == -1)
{
perror("munmap");
exit(EXIT_FAILURE);
}
}
// 6. 释放共享内存对象
shm_unlink(shm_name);
return 0;
}
5. 套接字(Sockets)
套接字是一种更为通用的进程间通信机制,它不仅可用于同一主机上的进程间通信,还可用于不同主机间的网络通信。套接字可以基于不同的协议族(如UNIX域套接字、TCP/IP套接字等)和类型(如流式套接字、数据报套接字等)来实现。
代码 MakeFile 编写
CC := gcc
FAILNAME:FALINAME.c # FAILNAME : 文件名
-$(CC) -o $@ $^
-./$@
-rm ./$@