linux应用开发学习:进程通信方式

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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值