1. 写在前面

之前的文章总结了使用管道进行进程间通信的方法,除了pipe和fifo,Linux内核还为我们提供了其他更高级的IPC方式,包括共享内存,消息队列,信号量等,本篇文章会通过一个具有完整逻辑功能的示例说明如何使用这些IPC方法。毕竟单纯地查手册,写代码...周而复始,这个过程还是比较枯燥的,而且并没有哪个IPC方法能解决所有的进程间通信问题,每种方法都不是孤立存在的,通过一个小例子把它们串联起来,是一种更好的学习方式。下文中的代码实现可以参考我的代码仓库

2. POSIX IPC概述

进程间通信,主要解决两个问题,即数据传递和同步。POSIX IPC提供了下面三种方法:

  • 消息队列
  • 共享内存
  • 信号量

操作系统中运行的进程,彼此之间是隔离的,要想实现通信,就必须有一个媒介,是通信双方都可以访问到的。从这个角度看,操作系统内核正是每个进程都可以访问到的那个媒介,就像一个"全局变量"。消息队列不过是内核维护的一个队列,保存了用户进程发送来的消息,其他进程可以从队列中取走消息,每个消息可以设置优先级,进程发送和接收消息的行为可以是阻塞或者非阻塞的,这一点类似管道;共享内存就是利用了虚拟地址空间以及物理地址空间,让不同进程的虚拟地址映射到同一个物理页面上,这样就实现了共享,对于映射的地址空间可以设定可读,可写以及可执行的标志;信号量就像一个内核中的整型变量,这个变量的数值记录了某种资源的数量,进程可以对它进行加减操作,合理使用的话就能完成想要的进程之间的同步逻辑。可以看到,这三种IPC方法在内核中都对应了一种数据结构,为了能够让用户进程访问到这些数据结构,POSIX IPC延续了“一切皆文件”的设计思路,我们可以用类似“/somename”这种形式的文件名去创建或者打开这些IPC对象,然后对它们进行各种操作,和文件的访问权限类似,进程操作IPC对象时也会进行权限检查。可能上面对三种POSIX IPC的描述存在不严谨的地方,但对于使用者来说,我们只要在脑子里建立一个合适的,能够描述它们工作方式的模型就可以了,而不是不断重复手册中对每个api的叙述。下面的表格列出了常用的POSIX IPC api:

消息队列 共享内存 信号量
打开 mq_open shm_open sem_open
关闭 mq_close shm_close sem_close
操作 mq_send/mq_receive 内存读写 sem_wait/sem_post
删除 mq_unlink shm_unlink sem_unlink

3. POSIX IPC使用

3.1 项目功能说明

下面将使用三种POSIX IPC实现一个简单的项目,用来记录IPC的使用方法。项目包含一个server进程和若干个client进程,他们各自的功能如下:

  • server进程

    • 首先运行,等待client的连接到来;
    • 收到client的连接,fork出一个新的进程去处理client的请求;
  • client进程
    • 可以同时运行多个;
    • 启动时和server建立连接,连接建立完成之后,接受用于输入,向server发起请求;
    • 可以完成主动断开连接,终止server进程,以及其他操作;

首先启动server进程,然后启动多个client进程向server发送请求,项目实现之后的效果如下:

3.2 项目实现原理

  1. client如何和server建立连接?

    client进程和server进程都可以访问一段共享内存,当server进程启动时,会对这段共享内存进行初始化,初始化完成之后,server对信号量A执行post操作,表明共享内存准备完毕,之后server进程就通过信号量B等待新连接的建立;当有新的client进程想建立连接时,会先通过对信号量A执行wait操作,等待共享内存可用,如果可用,client会把请求参数写到共享内存之中,写入完成后会对信号量B执行post操作,通知server进程有新的连接已经建立。

  2. client建立连接之后如何发送请求?

    client通过两个消息队列实现发送请求和接收响应。在client建立连接时,会在共享内存中写入用于和server通信的两个消息队列的名字,server在处理连接时会打开消息队列,然后和client进行通信。对于每个新建立的连接,server会fork出一个新的进程去处理该连接对应的client发送来的请求。

  3. client如何通过发送请求关闭server?

    client通过向请求消息队列中写入kill_server请求,可以实现关闭server。当server进程fork出的进程从消息队列中读到kill_server请求,该进程会通过管道写入数据,通知server的主进程结束运行。

  4. server和client之间的时序关系:

    通过前面3点的描述可以看出,这个简单的项目几乎用到了全部常用的IPC方法,下面这个时序图更直观地说明了其工作原理:

    server和client之间的同步操作主要集中在步骤6,7,8,9。当server准备好共享内存之后,通过第6步的信号量A通知client可以建立连接了,之后client向共享内存写入数据,再操作第9步的信号量B通知server连接数据已经写入,最后server会创建子进程去处理client的请求。实际上server的主进程是一个循环,处理请求都是在server的子进程中完成的,以上内容说明了server主进程在循环中完成的工作。

  5. 资源清理

    当我们使用POSIX IPC时,内核会建立相应的数据结构,并且通过文件系统接口展示给用户,但IPC资源不能无限创建,当我们的程序运行结束之后应该清理自己用到的IPC资源。运行程序时创建的POSIX IPC对象可以在/dev/shm以及/dev/mqueue下查看,程序结束之后,server和client会释放掉自己创建的IPC资源。所以,要查看server和client创建的共享内存,信号量以及消息队列,需要在程序运行期间查看上述的两个目录。

3.3 主要代码功能

  • 消息格式:

    server和client之间通过消息队列传递请求和响应数据,消息队列中消息格式定义如下:

    struct msgbuf {
    int type;
    union {
    struct {
    int a;
    int b;
    } request_add; struct {
    int c;
    } response_add; struct {
    int disconect;
    } request_disconnect; struct {
    int kill_server;
    } request_kill_server;
    } data;
    };
  • server主进程:

    int main(int argc, char **argv) {
    
        int err = server_init();
    if (err) {
    log_warning("server_init failed\n");
    return -1;
    }
    server_start();
    server_shutdown(); return 0;
    }

    其中,在server_init中,server会创建需要使用的共享内存,信号量以及管道。

    int server_init() {
    memset(&ipc_server, 0, sizeof(ipc_server)); // shared memory init
    ipc_server.conn_buf_fd =
    shm_open(CONNECTION_SHM, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR); ... if (ftruncate(ipc_server.conn_buf_fd, CONNECTION_SHM_SIZE) < 0) {
    log_warning("server failed ftruncate\n");
    return -1;
    } ipc_server.conn_buf = (struct connection *)mmap(
    NULL, CONNECTION_SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED,
    ipc_server.conn_buf_fd, 0); ... memset(ipc_server.conn_buf, 0, CONNECTION_SHM_SIZE); ipc_server.conn_buf_ready =
    sem_open(CONNECTION_BUF_SEM, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR, 0); ... ipc_server.conn_new_ready =
    sem_open(CONNECTION_NEW_SEM, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR, 0); ... // pipe init
    int pipefd[2];
    if (pipe2(ipc_server.pipefd, O_NONBLOCK)) {
    log_warning("server failed pipe2\n");
    return -1;
    } log_info("server init done\n");
    return 0;
    }

    在server_start中会循环处理来自client的连接。

    void server_start() {
    int err = sem_post(ipc_server.conn_buf_ready); ... struct connection conn;
    int stop = 0;
    while (!stop) {
    // handle new connection
    sem_wait(ipc_server.conn_new_ready);
    if (read(ipc_server.pipefd[0], &stop, sizeof(int)) <= 0)
    stop = 0; if (ipc_server.conn_buf->valid) {
    log_info("new connection established\n");
    memcpy(&conn, ipc_server.conn_buf, sizeof(conn));
    handle_connection(&conn);
    memset(ipc_server.conn_buf, 0, sizeof(struct connection));
    sem_post(ipc_server.conn_buf_ready);
    }
    }
    }

    当server主进程退出之后,server_shutdown会清理IPC资源。

  • client进程:

    client启动之后,首先会尝试和server建立连接,建立连接之后会循环处理用户输入,通过消息队列向server的服务进程发送请求。

    int main(int argc, char **argv) {
    
        if (build_connection()) {
    log_info("client failed build_connection\n");
    return -1;
    }
    handle_command();
    cleanup(); log_info("client %d exit\n", getpid());
    return 0;
    }

    client建立连接的过程如下:建立连接时client需要等待共享内存可用,并且在写入连接数据之后通知server,这些同步操作都是通过信号量实现的。

    int build_connection() {
    
        ...
    
        connection.mqreq_fd =
    mq_open(connection.mqreq, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR, &attr);
    connection.mqrsp_fd =
    mq_open(connection.mqrsp, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR, &attr); ... // open and map shared memory
    int fd = shm_open(CONNECTION_SHM, O_RDWR, 0);
    void *conn_buf = mmap(NULL, CONNECTION_SHM_SIZE, PROT_READ | PROT_WRITE,
    MAP_SHARED, fd, 0); // write connection to conn_buf, notify server new conncection is comming
    sem_t *conn_buf_ready = sem_open(CONNECTION_BUF_SEM, O_RDWR);
    sem_t *conn_new_ready = sem_open(CONNECTION_NEW_SEM, O_RDWR); ... sem_wait(conn_buf_ready);
    connection.valid = 1;
    memcpy(conn_buf, &connection, sizeof(connection));
    sem_post(conn_new_ready); ... return 0;
    }

写在最后

通过一个包含server和client的代码示例,说明了POSIX IPC中共享内存,消息队列以及信号量的使用方法。具体实现可以参考我的代码仓库

linux环境编程(3): 使用POSIX IPC完成进程间通信的更多相关文章

  1. Linux环境编程相关的文章

    Linux环境编程相关的文章 好几年没有接触Linux环境下编程了,好多东西都有点生疏了.趁着现在有空打算把相关的一些技能重拾一下,顺手写一些相关的文章加深印象. 因为不是写书,也受到许多外部因素限制 ...

  2. Linux环境编程之同步(四):Posix信号量

    信号量是一种用于提供不同进程间或一个给定进程的不同线程间同步手段的原语.有三种类型:Posix有名信号量,使用Posix IPC名字标识.Posix基于内存的信号量,存放在共享内存区中:System ...

  3. 二、linux IO 编程---系统调用和POSIX标准和标准IO

    2.1 系统调用 2.1.1 概念 所谓系统调用(system call)是指曹错系统提供给用户程序的一组“特殊”接口,用户程序可以通过这组“特殊”接口来获得操作系统内核提供的特殊服务. 应用程序可以 ...

  4. Linux 环境编程:dirfd参数 有关解析

    背景 在Unix环境编程中,系统提供了很多以at结尾的函数,如openat.fstatat等,而这类函数通常有一个特点,就是形参列表中多了int dirfd 例如: int open(const ch ...

  5. Linux环境编程--waitpid与fork与execlp

    waitpid waitpid(等待子进程中断或结束) 表头文件 #include<sys/types.h> #include<sys/wait.h> 定义函数 pid_t w ...

  6. Linux环境编程之共享内存区(一):共享内存区简单介绍

    共享内存区是可用IPC形式中最快的.一旦内存区映射到共享它的进程的地址空间,进程间数据的传递就不再涉及内核.然而往该共享内存区存放信息或从中取走信息的进程间通常须要某种形式的同步.不再涉及内核是指:进 ...

  7. Linux环境编程进程间通信机制理解

    一.Linux系统调用主要函数 二.创建进程 1.创建子进程系统调用fork() 2.验证fork()创建子进程效果 3.系统调用fork()与挂起系统调用wait() 三.模拟进程管道通信 四.pi ...

  8. 笔记整理:计算CPU使用率 ----linux 环境编程 从应用到内核

    linux 提供time命令统计进程在用户态和内核态消耗的CPU时间: [root@localhost ~]# time sleep real 0m2.001s user 0m0.001s sys 0 ...

  9. Linux环境编程导引

    计算机系统硬件组成 总线 贯穿整个系统的一组电子管道称为总线, 分为: 片内总线 系统总线 数据总线DB 地址总线AB 控制总线CB 外部总线 I/O设备 I/O设备是系统与外界联系的通道 键盘鼠标是 ...

  10. 【Linux环境编程】获取网卡的实时网速

    在windows以下.我们能够看到360或者是qq安全卫士的"安全球".上面显示实时的网速情况.那么在linux里面怎样获取网卡的实时网速?事实上原理非常easy,读取须要获取网速 ...

随机推荐

  1. pyinstaller 打包exe相关

    -w 只有窗口,没有console -p 加入路径 -F 生成一个exe文件 有虚拟环境时,需要先在cmd中进入虚拟环境,再执行打包程序 # 生成一个exe 无窗口 有icon Pyside2 pyi ...

  2. 实现将机器A上的程序包复制到机器B并更新的脚本

    一.前言 之前有写过如何在单台服务器上执行脚本自动更新程序包,但平时测试过程中相信大部分公司都是需要测试人员在服务器A上进行功能测试,测试通过后再将程序包更新到服务器B上进行安全测试或者性能测试:今天 ...

  3. 棋盘覆盖(java实现)

    棋盘覆盖 问题描述 在一个2k×2k 个方格组成的棋盘中,恰有一个方格与其它方格不同,称该方格为一特殊方格,且称该棋盘为一特殊棋盘.在棋盘覆盖问题中,要用图示的4种不同形态的L型骨牌覆盖给定的特殊棋盘 ...

  4. 自学 TypeScript 第二天 编译选项

    前言: 昨天我们学习了 TS 的数据类型,不知道大家回去以后练习没练习,如果你练习了一定会发现一个问题,我们的 TS 好像和 JS 不太一样 JS 写完之后直接就可以放到页面上,就可以用了,而我们的 ...

  5. 【iOS逆向】某营业厅算法分析

    阅读此文档的过程中遇到任何问题,请关注公众号[移动端Android和iOS开发技术分享]或加QQ群[812546729] 1.目标 使用frida stalker分析某营业厅的签名算法. 2.操作环境 ...

  6. JqGrid 编辑单元格内容时提示url未设定错误 2018-08-06

    感谢大佬的资料https://blog.csdn.net/Easy_____/article/details/30218421 虽然没实例,但也给了一些信息.我以为cellsubmit属性是添加到co ...

  7. 初次邂逅 EasyExcel

    前言 由于工作原因,有这种需求,就是把数据库中的数据导出成 Excel 表格,同时,也得支持人家用 Excel 表格导入数据到数据库.当前项目也是在用 EasyExcel,所以我不得不学啦! 以前学习 ...

  8. 微信小程序实战,基于vue2实现瀑布流

    1.什么是瀑布流呢? 瀑布流,又称瀑布流式布局.是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部. 瀑布流对于图片的展现, ...

  9. Java框架--SSM&Oracle&Maven高级

    〇.内容介绍 一.MyBatis01:框架概述.环境搭建及入门案例.自定义框架 1.介绍 框架:封装细节,是开发中的解决方案 三层架构与SSM的关系 表示层web:SpringMVC框架 业务层ser ...

  10. Python数据类型+运算符

    Python基础数据类型 上期练习讲解 # 练习一.想办法打印出jason l1 = [11, 22, 'kevin', ['tony', 'jerry', [123, 456, 'jason'] ] ...