1、匿名管道
1.1、核心特点
1、亲缘关系限制:仅能用于有亲属关系的进程中
2、单向通信:管道是半双工的,无法双向通信,想要双向通信必须建议两个管道
3、先进先出(FIFO):数据按写入顺序读取,类似队列,数据读取后就会从管道删除
4、阻塞特性:若管道为空,数据无法读取,除非写入管道数据,同理若管道满了,数据无法写入,除非读取出管道数据,才能继续写入
1.2、函数说明
int pipe (int __pipedes[2])
生成匿名管道的函数,内部需要传入一个两个参数的数组,用来存储管道的文件描述符,第一个参数为读,第二个参数为写
1.3、案例展示
本案例首先是定义了一个文件描述符pipefd[0]为读文件描述符,pipefd[1]为写文件描述符,随后判断用户输入是否是两个参数,不是的话,提醒用户需传递正确信息,如果pipe(pipefd)管道创建失败,则退出程序,然后创建父子进程,子进程用来读取,父进程用来写入,在父进程写入段中,我们首先关闭管道的读取通道,写完后关闭写通道,等待子进程读取,子进程段,读取管道数据并且打印到终端,完成后子进程退出,随后父进程退出。
#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[])
{
int pipefd[2];
//将程序传进来的第一个命令行参数,通过管道传输给子进程
if(argc != 2)
{
fprintf(stderr,"%s请填写需要传递的信息\n",argv[0]);
exit(EXIT_FAILURE);
}
if(pipe(pipefd) == -1)
{
perror("创建管道失败");
exit(EXIT_FAILURE);
}
pid_t cpid = fork();
if(cpid == -1)
{
perror("fork");
exit(EXIT_FAILURE);
}
else if(cpid == 0)
{
//子进程 读取管道的数据 打印到控制台
close(pipefd[1]);
char *str = "新学员接收信息";
write(STDOUT_FILENO,str,strlen(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;
}
2、有名管道
2.1、核心特点
1、存在于文件系统中,有具体路径名:有名管道通过mkfifo创建,可以适用于无亲缘关系的进程
2.、半双工通信,数据单向流动
3、 基于文件描述符操作,遵循 FIFO 规则
4、阻塞特性:若管道为空,数据无法读取,除非写入管道数据,同理若管道满了,数据无法写入,除非读取出管道数据,才能继续写入。只有当读写进程都成功打开管道后,双方才能开始通信
2.2、函数说明
int mkfifo (const char *__path, __mode_t __mode)
生成有名管道的函数,第一个参数填写有名管道文件路径,第二个参数为有名管道的访问权限,例如0664
2.3、案例展示
本案例是构建了两个文件,一个是读文件,一个是写文件,写文件负责从终端读取你键盘写入的文件到管道中,读文件负责从管道中读取你写入的文件,并打印到终端。
2.2.1管道写入
代码流程为先设置管道的路径以及名称,用pipe_path来存储,然后构建管道,如果构建失败则弹出mkfifo错误,并判断是否是管道存在导致的构建失败,如果不是因为管道存在导致的失败,就退出执行程序,定义fd来标识我们用open打开的管道文件,判断打开是否成功。成功就对管道进行写入操作,read_num = 0时,代表写入完毕,关闭文件描述符fd,并且清理掉管道。清理管道要记住,谁创建 / 主导,谁清理。
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main(int argc, char const *argv[])
{
//有名管道创建完成之后是可以重复使用的,不推荐重复使用,推荐使用完之后释放掉
char *pipe_path = "/tmp/myfifo";
if(mkfifo(pipe_path,0664) != 0)
{
perror("mkfifo");
if(errno != 17) //管道已经存在的话,不会创建新的管道,所以会进入到此函数里,然后errno != 17实际上
//就是管道不存在的情况,意味着管道没创建成功,有错误,所以关闭程序
{
exit(EXIT_FAILURE);
}
}
//对有名管道的特殊文件 创建fd
int fd = open(pipe_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(pipe_path) == -1)
{
perror("unlink");
}
return 0;
}
2.2.2管道读取
代码流程前部分同上写入操作,读取部分,是设置一个中间变量,将数据从管道中读取,再打印到终端,读取完成后发送接受管道数据成功,进程终止,并关闭文件描述符fd。
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main(int argc, char const *argv[])
{
char *pipe_path = "/tmp/myfifo";
if(mkfifo(pipe_path,0664) != 0)
{
perror("mkfifo");
exit(EXIT_FAILURE);
}
//对有名管道的特殊文件 创建fd
int fd = open(pipe_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);
//write(stdout,buf,read_num);
}
if(read_num < 0)
{
perror("read");
close(fd);
exit(EXIT_FAILURE);
}
printf("接收管道数据完成,进程中止\n");
close(fd);
return 0;
}
3、共享内存通信
3.1、核心特点
1、效率极高(核心优势):共享内存直接将一块内核内存映射到多个进程的地址空间,进程通过指针直接读写该内存,不用进入到内核态
2、无数据大小限制(仅受系统内存限制):共享内存的大小由用户通过ftruncate或mmap指定,仅受限于系统可用内存和进程地址空间大小
3、生命周期独立:共享内存由内核管理,其生命周期不依赖于创建它的进程:即使创建进程退出,只要还有其他进程在使用(映射)该内存,它就不会被释放。需显式清理:通过shm_unlink删除共享内存的名称,待最后一个进程解除映射munmap后,内核才会真正释放其占用的内存。
4、适用于亲缘或非亲缘进程
3.2、函数说明
1、int shm_open (const char *__name, int __oflag, mode_t __mode)
用来打开/创建一个共享内存,第一个参数为共享内存的名称,第二个参数为打开/创建的模式,例如只读,只写,创建等,第三个参数为权限
2、ftruncate
用来设置共享内存的大小,第一个参数为文件描述符,第二个参数为要设置的大小
3、void *mmap (void *__addr, size_t __len, int __prot, int __flags, int __fd, __off_t __offset)
用来将共享内存映射到当前进程的地址中去,第一个参数为映射的起始地址,通常设为null,第二个参数为映射的内存大小,第三个参数为映射区域的访问权限,第四个参数为映射的和新特性,此处为共享内存通信所以应该设置MAP_SHARED :共享映射,第五个参数为文件描述符fd,第六个参数是起始位置偏移字节数,一般为0
4、int munmap (void *__addr, size_t __len)
用来释放共享内存,第一个参数为需要解除映射的起始地址,第二个参数为需解除的内存大小
3.3、案例展示
本案例实现了父子进程之间通过共享内存来进行通信的操作,代码流程为先创建一个共享内存的名称,必须以'/'开头,利用shm_open创建共享内存,判断共享内存创建是否有问题,没有问题利用ftruncate设置共享内存对象的大小,再利用mmap将共享内存映射到当前进程地址中去,mmap成功时,返回一个内存地址,我们定义一个变量share,存储这个地址,映射完成后关闭fd,fd用不到了,然后创建父子进程,子进程将内容写入到share中,父进程等待子进程运行完毕后,读取share内容,打印到终端,任务完成后,释放映射区,释放共享内存对象。
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char const *argv[])
{
char *share;
//1、创建一个共享内存对象
char shm_name[100] ={0};
sprintf(shm_name,"/letter%d",getpid());
int fd = shm_open(shm_name,O_RDWR | O_CREAT,0664);
if(fd < 0)
{
perror("shm_open");
exit(EXIT_FAILURE);
}
//2、设置共享内存对象的大小
ftruncate(fd,1024);
//3、内存映射
share = mmap(NULL,1024,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
if(share == MAP_FAILED)
{
perror("mmap");
exit(EXIT_FAILURE);
}
//映射完成后需要关闭fd连接,fd后续用不到了,不是释放
close(fd);
//4、使用内存映射实现进程之间的通信
pid_t pid = fork();
if(pid < 0)
{
perror("fork");
exit(EXIT_FAILURE);
}
else if(pid == 0)
{
//子进程
strcpy(share,"你是个好人");
printf("新学员%d完成回信\n",getpid());
}
else
{
//父进程
waitpid(pid,NULL,0);
printf("老学员%d收到了新学员%d回信:%s\n",getpid(),pid,share);
}
//5、释放映射区
int re = munmap(share,1024);
if(re == -1)
{
perror("munmap");
exit(EXIT_FAILURE);
}
//6、释放共享内存对象
shm_unlink(shm_name);
return 0;
}
4、消息队列
4.1、核心特点
1、结构化传输:以 “消息类型 + 数据” 的独立包传输,接收方可按类型筛选,无需处理无边界字节流;
2、异步解耦:发送方发完即返,消息由内核暂存,接收方可后续读取,进程无需同步运行;
3、内核托管:生命周期独立于进程,内核负责存储与权限管理,操作仅需系统调用;
4、大小受限:单条消息、队列总容量均有内核限制,不适合超大 / 高频数据;
5、轻量同步:默认空队列读阻塞、满队列写阻塞,无需手动设计基础同步;
6、跨进程通用:通过键值标识,亲缘 / 非亲缘进程均可关联,适用范围广。
4.2、函数说明
1、mq_open函数
定义:mq_open(const char *name, int oflag, mode_t mode, struct mq_attr *attr)
第一个参数:const char *name 消息队列名称,必须以/开头
第二个参数:int oflag 打开方式标志
第三个参数:mode_t mode 权限位,读写权限例如0664
第四个参数:struct mq_attr *attr 消息队列属性结构体指针
2、定义消息队列属性结构体:struct mq_attr attr;
attr.mq_maxmsg = 10; // 消息队列最多能容纳10条消息
attr.mq_msgsize = 100; // 每条消息最大长度为100字节
attr.mq_curmsgs = 0; // 当前消息数量,初始化时设为0(实际创建后由系统管理)
attr.mq_flags = 0; // 消息队列标志,0表示默认的阻塞模式
3、mq_timedsend(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned int msg_prio, const struct timespec *abs_timeout)
是一个带超时机制发送消息的函数,第一个参数是消息队列描述符,第二个参数是存储要发送的数据,第三个参数是发送消息的长度,第四个参数是消息优先级,第五个参数是绝对超时时间
4、mq_timedreceive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned int *msg_prio, const struct timespec *abs_timeout)
是一个带超时机制接收消息的函数,参数同上,发送需改为接收
4.3、案例展示
本案例实现了父子进程,通过消息队列来读写数据,父进程循环 10 次发送消息(每条设置 5 秒发送超时),子进程对应接收 10 条消息(每条设置 15 秒接收超时),通信结束后由父进程清理消息队列资源。
#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_curmsgs = 0;
attr.mq_flags = 0;
char *mq_name = "/father_son_mq";
//消息队列描述符mqd_t 实际上跟fd文件描述符一个意思
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");
}
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(CLOCK_REALTIME, &time_info);
time_info.tv_sec += 15;
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;
clock_gettime(0,&time_info);
for(int i = 0;i<10;i++)
{
//清空处理buf
memset(send_buf,0,100);
sprintf(send_buf,"父进程的第%d次发送消息",i+1);
//获取当前的具体时间
clock_gettime(CLOCK_REALTIME, &time_info);
time_info.tv_sec += 5;
if((mq_timedsend(mqdes,send_buf,strlen(send_buf),0,&time_info)) == -1)
{
perror("mq_timedsend");
}
printf("父进程发送一条消息,休眠1s\n");
sleep(1);
}
}
//最终不管是父进程还是子进程都需要释放消息队列的引用
close(mqdes);
//清除消息队列只需要执行一次
if(pid > 0)
{
mq_unlink(mq_name);
}
return 0;
}