Linux —— 进程间通信(2)

简介: Linux —— 进程间通信(2)

4.管道的读写规则

int pipe(int pipefd[2]);
int pipe2(int pipefd[2], int flags);

60a6bcefe26f4b118e50f46e4d0afd1d.png

当没有数据可读时


O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。

O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。

当管道满的时候


O_NONBLOCK disable: write调用阻塞,直到有进程读走数据

O_NONBLOCK enable:调用返回-1,errno值为EAGAIN

如果所有管道写端对应的文件描述符被关闭,则read返回0

如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出

当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。

当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。  

60a6bcefe26f4b118e50f46e4d0afd1d.png

四、命名管道

       匿名管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。

1.命名管道的创建

1.命名行创建

命名管道可以从命令行上创建,命令行方法是使用下面这个命令:

[mlg@VM-20-8-centos lesson6-进程间通信]$ mkfifo myfifo

60a6bcefe26f4b118e50f46e4d0afd1d.png

       我们创建好命令管道后,就可以实现两个进程间的通信了;(左图的进程进行循环的数据写入,右图进程进行读取)当我们关闭读端的时候,写端也会被操作系统关闭,当我们关闭写端时,读端会一直在等写端;

60a6bcefe26f4b118e50f46e4d0afd1d.png

当然也可以让读端不断的读取数据,写端只要写就行了()

60a6bcefe26f4b118e50f46e4d0afd1d.png

2.程序创建(mkfifo函数)

在程序中创建命名管道使用mkfifo函数,mkfifo函数的函数原型如下:

int mkfifo(const char *pathname, mode_t mode);

pathname:表示你要创建的命名管道文件

  • 如果pathname是以文件的方式给出,默认在当前的路径下创建;
  • 如果pathname是以某个路径的方式给出,将会在这个路径下创建;

mode:表示给创建的命名管道设置权限

我们在设置权限时,例如0666权限,它会受到系统的umask(文件默认掩码)的影响,实际创建出来是(mode & ~umask)0664;

image.png

所以想要正确的得到自己设置的权限(0666),我们需要将文件默认掩码设置为0;

75f0e2306cfe4b549332ab598e15c984.png

返回值:命名管道创建成功返回0,失败返回-1

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#define MY_FIFO "myfifo"      //默认是在当前路径下创建
//#define MY_FIFO "../xxx/myfifo" //指定在上级目录下的xxx目录下创建
int main()
{
    umask(0);                                                                                                                           
    if(mkfifo(MY_FIFO, 0666) < 0)
    {
         perror("mkfifo");
         return 1;
    }
    return 0;
}

60a6bcefe26f4b118e50f46e4d0afd1d.png

2.命名管道的打开规则

如果当前打开操作是为读而打开FIFO时

O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO

O_NONBLOCK enable:立刻返回成功

如果当前打开操作是为写而打开FIFO时

O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO

O_NONBLOCK enable:立刻返回失败,错误码为ENXIO

3.用命名管道实现server&client通信

       实现server(服务端)和client(客户端)之间的通信,我们让server创建命名管道,用来读取命名管道内的数据;client获取管道,用来向命名管道内写数据;server(服务端)和client(客户端)想要使用同一个管道,这里我们可以让客户端和服务端包含同一个头文件comm.h,该头文件当中提供这个共用的命名管道文件的文件名,这样客户端和服务端就可以通过这个文件名,打开同一个命名管道文件,进而进行通信了。

60a6bcefe26f4b118e50f46e4d0afd1d.png

comm.h:
#pragma once
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>                                              
#define MY_FIFO "./fifo" 

server.c:

#include "comm.h"
int main()
{
    umask(0); //将文件掩码设置为0,确保得到我们设置的权限
    if(mkfifo(MY_FIFO, 0666) < 0){ //服务端用来创建命名管道文件
        perror("mkfifo");
        return 1;
    }
    int fd = open(MY_FIFO, O_RDONLY); //以只读的方式打开命名管道文件
    if(fd < 0){
        perror("open");
        return 2;
    }
    while(1){
        char buffer[64] = {0};
        ssize_t s = read(fd, buffer, sizeof(buffer) - 1); //从fd(命名管道)中读数据到buffer中
        if(s > 0){                                                                                                                 
            buffer[s] = 0;
            printf("client: %s\n", buffer); //打印客户端发来的数据
        }
        else if(s == 0){
            printf("client qiut...\n");
            break;
        }
        else{
            perror("open");
            break;
        }
    }
    close(fd); //通信结束,关闭命名管道文件
    return 0;
 } 

client.c:

#include "comm.h"
int main()
{
    //这里不需要创建fifo,只需要获取就行
    int fd = open(MY_FIFO, O_WRONLY); //以写的方式打开命名管道文件
    if(fd < 0){ 
        perror("open");
        return 1;
    }
    //业务逻辑
    while(1){
        printf("请输入:");
        fflush(stdout);
        char buffer[64] = {0};
        //先把数据从标准输入拿到我们的client进程内部
        ssize_t s = read(0, buffer, sizeof(buffer) - 1);
        if(s > 0){
            buffer[s-1] = 0;
            printf("%s\n",buffer);
            //拿到了数据,将数据写入命名管道
            write(fd, buffer, strlen(buffer));
        }
    }
    close(fd); //通信完毕,关闭命名管道文件
    return 0;
}

编写Makefile:

60a6bcefe26f4b118e50f46e4d0afd1d.png

       接下来使用Makefile进行编译,然后我们需要先将服务端运行起来,再运行客户端,因为服务端是用来创建命名管道文件的,先运行客户端的话,是不可以打开一个不存在的文件的;

60a6bcefe26f4b118e50f46e4d0afd1d.png

4.用命名管道实现client控制server执行某种任务

       两个进程间的通信,不是只能发送一些字符串,还可以实现一个进程控制另一个进程去完成某种任务;比如:client(客户端)向让server(服务端)执行“显示当前目录下的所有文件信息”的任务和执行“小火车命令sl”

#include "comm.h"
int main()
{
  umask(0); //将文件掩码设置为0,确保得到我们设置的权限
  if (mkfifo(MY_FIFO, 0666) < 0) { //服务端用来创建命名管道文件
    perror("mkfifo");
    return 1;
  }
  int fd = open(MY_FIFO, O_RDONLY); //以只读的方式打开命名管道文件
  if (fd < 0) {
    perror("open");
    return 2;
  }
  while (1) {
    char buffer[64] = { 0 };
    ssize_t s = read(fd, buffer, sizeof(buffer) - 1); //从fd(命名管道)中读数据到buffer中
    if (s > 0) {
      buffer[s] = 0;
            //client控制server完成某种动作/任务
      if (strcmp(buffer, "show") == 0) {
        if (fork() == 0) {
          execl("/usr/bin/ls", "ls", "-l", NULL);
          exit(1);
        }
        waitpid(-1, NULL, 0);
      }
      else if (strcmp(buffer, "run") == 0) {
        if (fork() == 0) {
          execl("/usr/bin/sl", "sl", NULL);
        }
      }
      else {
        printf("client: %s\n", buffer);
      }
    }
    else if (s == 0) {
      printf("client qiut...\n");
      break;
    }
    else {
      perror("open");
      break;
    }
  }
  close(fd); //通信结束,关闭命名管道文件
  return 0;
}

客户端输入show之后,服务端就显示数当前目录下的所有文件

60a6bcefe26f4b118e50f46e4d0afd1d.png

客户端输入run之后,服务端就让小火车跑起来了

60a6bcefe26f4b118e50f46e4d0afd1d.png

5.管道的总结

管道:


管道分为匿名管道和命名管道;

管道通信方式的中间介质是文件,通常称这种文件为管道文件;

匿名管道:管道是半双工的,数据只能单向通信;需要双方通信时,需要建立起两个管道;只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程)。

命名管道:不同于匿名管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信

利用系统调用pipe()创建一个无名管道文件,通常称为无名管道或PIPE;利用系统调用mknod()创建一个命名管道文件,通常称为有名管道或FIFO。

PIPE是一种非永久性的管道通信机构,当它访问的进程全部终止时,它也将随之被撤消。

FIFO是一种永久的管道通信机构,它可以弥补PIPE的不足。管道文件被创建后,使用open()将文件进行打开,然后便可对它进行读写操作,通过系统调用write()和read()来实现。通信完毕后,可使用close()将管道文件关闭。

匿名管道的文件是内存中的特殊文件,而且是不可见的,命名管道的文件是硬盘上的设备文件,是可见的。

五、system V进程间通信

它是操作系统层面上专门为进程间通信设计的一个方案,其通信方式包括如下三种:


system V共享内存

system V消息队列

system V信号量

       其中共享内存和消息队列是以传输数据为目的的,信号量是为了保证进程间的同步与互斥而设计的;本篇主要针对共享内容进行介绍

1.system V共享内存

1.共享内存的基本原理(示意图)

       不同的进程想要看到同一份资源,在操作系统内部,一定是通过某种调用,在物理内存当中申请一块内存空间,然后通过某种调用,让参与通信进程“挂接”到这份新开辟的内存空间上;其本质:将这块内存空间分别与各个进程各自的页表之间建立映射,再在虚拟地址空间当中开辟空间并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起对应关系,至此这些参与通信进程便可以看到了同一份物理内存,这块物理内存就叫做共享内存。

60a6bcefe26f4b118e50f46e4d0afd1d.png

2.共享内存的数据结构

       我们知道在操作系统中是存在大量的进程的,如果两两进程进程进行通信,就需要多个共享内存。既然共享内存在系统中存在多份,就一定要将这些不同的共享内存管理起来,即先描述,再组织;为了保证两个或多个进程能够看到它们的同一份共享内存,那么共享内存一定要有能够唯一标识性的ID,方便让不同的进程识别它们的同一份共享内存;这个所谓的ID一定是在共享内存的数据结构中;

struct shmid_ds {
    struct ipc_perm shm_perm;    /* operation perms */
    int shm_segsz;               /* size of segment (bytes) */
    __kernel_time_t shm_atime;   /* last attach time */
    __kernel_time_t shm_dtime;   /* last detach time */
    __kernel_time_t shm_ctime;   /* last change time */
    __kernel_ipc_pid_t shm_cpid; /* pid of creator */
    __kernel_ipc_pid_t shm_lpid; /* pid of last operator */
    unsigned short shm_nattch;   /* no. of current attaches */
    unsigned short shm_unused;   /* compatibility */
    void *shm_unused2;           /* ditto - used by DIPC */
    void *shm_unused3;           /* unused */
};
/*
    shm_perm   成员储存了共享内存对象的存取权限及其它一些信息。
    shm_segsz  成员定义了共享的内存大小(以字节为单位) 。
    shm_atime  成员保存了最近一次进程连接共享内存的时间。
    shm_dtime  成员保存了最近一次进程断开与共享内存的连接的时间。
    shm_ctime  成员保存了最近一次 shmid_ds 结构内容改变的时间。
    shm_cpid   成员保存了创建共享内存的进程的 pid 。
    shm_lpid   成员保存了最近一次连接共享内存的进程的 pid。
    shm_nattch 成员保存了与共享内存连接的进程数目
*/

对于每个IPC对象,系统共用一个struct ipc_perm的数据结构来存放权限信息,以确定一个ipc操作是否可以访问该IPC对象。

struct ipc_perm{
  __kernel_key_t  key;
  __kernel_uid_t  uid;
  __kernel_gid_t  gid;
  __kernel_uid_t  cuid;
  __kernel_gid_t  cgid;
  __kernel_mode_t mode;
  unsigned short  seq;
};

3.共享内存相关函数总览

image.png

4.共享内存的创建

创建共享内存我们需要用shmget函数,shmget函数的函数原型如下:

#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

函数说明:


得到一个共享内存标识符或创建一个共享内存对象并返回共享内存标识符

参数说明:


参数key:表示标识共享内存的键值


需要ftok函数获取

参数size:表示待创建共享内存的大小


size是要建立共享内存的长度。所有的内存分配操作都是以页为单位的。所以如果一段进程只申请一块只有一个字节的内存,内存也会分配整整一页(在32位下一页的缺省大小PACE_SIZE=4096字节);这样,新创建的共享内存的大小实际上是从size这个参数调整而来的页面大小。即如果 size为1至4096,则实际申请到的共享内存大小为4K(一页);4097到8192,则实际申请到的共享内存大小为8K(两页),依此类推。

参数shmflg:表示创建共享内存的方式

60a6bcefe26f4b118e50f46e4d0afd1d.png

shmflg主要和一些标志有关。
其中有效的包括IPC_CREAT和IPC_EXCL,它们的功能与open()的O_CREAT和O_EXCL相当。 
IPC_CREAT 如果共享内存不存在,则创建一个共享内存,否则打开操作。 
IPC_EXCL 只有在共享内存不存在的时候,新的共享内存才建立,否则就产生错误。
如果单独使用IPC_CREAT:
shmget()函数要么返回一个已经存在的共享内存的标识符 ,要么返回一个新建的共享内存的标识符。
如果将 IPC_CREAT和IPC_EXCL标志一起使用:
shmget()将返回一个新建的共享内存的标识符;如果该共享内存已存在,或者返回-1。
IPC_EXEL标志本身并没有太大的意义,但是和IPC_CREAT标志一起使用可以用来保证所得的对象是新建的,而不是打开已有的对象。

返回值:

  • 调用成功,返回一个有效的共享内存标识符。
  • 调用失败,返回-1,错误原因存于errno中

传入shmget函数的第一个参数key,需要我们使用ftok函数进行获取

#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
//把从pathname导出的信息与proj_id的低序8位组合成一个整数IPC键,传给shmget函数的key

       ftok函数的作用就是,将一个已存在的路径名pathname(此文件必须存在且可存取)和一个整数标识符proj_id转换成一个key值。在使用shmget函数创建共享内存时,首先要调用ftok函数获取这个key值,这个key值会被填充进维护共享内存的数据结构当中,作为共享内存的唯一标识。

结合上面的知识,我们就可以来创建共享内存了,代码如下:

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#define PATH_NAME "./" //路径名
#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096      //共享内存的大小
int main()
{
    key_t key = ftok(PATH_NAME, PROJ_ID);//获取key值
    if(key < 0){
        perror("ftok");
        return 1;
    }
    int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL);//创建共享内存
    if(shmid < 0){
        perror("shmget");
        return 2;
    }              
    printf("key: %u  shmid: %d\n", key, shmid);
    return 0;
}

60a6bcefe26f4b118e50f46e4d0afd1d.png

我们可以使用ipcs命令查看有关进程间通信设施的信息

60a6bcefe26f4b118e50f46e4d0afd1d.png

这里的key和上面打印出来的key是一样的,我们是以 无符号数10进制打印的;


       单独使用ipcs命令时,会默认列出消息队列、共享内存以及信号量相关的信息,若只想查看它们之间某一个的相关信息,可以选择携带以下选项:


-q:列出消息队列相关信息。

-m:列出共享内存相关信息。

-s:列出信号量相关信息。

其中:

  • key:共享内存的唯一键值
  • shmid:共享内存的编号
  • owner:创建的用户
  • perms:共享内存的权限
  • bytes:共享内存的大小
  • nattach:连接到共享内存的进程数
  • status:共享内存的状态

key vs shmid

key:只是用来在系统层面上进行标识唯一性的,不能用来管理共享内存;

shmid:是操作系统给用户返回的id,用来在用户层上进行管理共享内存;

key和shmid之间的关系类似于 fd 和 FILE* 之间的的关系。


目录
相关文章
|
2月前
|
资源调度 Linux 调度
Linux c/c++之进程基础
这篇文章主要介绍了Linux下C/C++进程的基本概念、组成、模式、运行和状态,以及如何使用系统调用创建和管理进程。
40 0
|
1天前
|
运维 监控 Linux
Linux操作系统的守护进程与服务管理深度剖析####
本文作为一篇技术性文章,旨在深入探讨Linux操作系统中守护进程与服务管理的机制、工具及实践策略。不同于传统的摘要概述,本文将以“守护进程的生命周期”为核心线索,串联起Linux服务管理的各个方面,从守护进程的定义与特性出发,逐步深入到Systemd的工作原理、服务单元文件编写、服务状态管理以及故障排查技巧,为读者呈现一幅Linux服务管理的全景图。 ####
|
26天前
|
缓存 监控 Linux
linux进程管理万字详解!!!
本文档介绍了Linux系统中进程管理、系统负载监控、内存监控和磁盘监控的基本概念和常用命令。主要内容包括: 1. **进程管理**: - **进程介绍**:程序与进程的关系、进程的生命周期、查看进程号和父进程号的方法。 - **进程监控命令**:`ps`、`pstree`、`pidof`、`top`、`htop`、`lsof`等命令的使用方法和案例。 - **进程管理命令**:控制信号、`kill`、`pkill`、`killall`、前台和后台运行、`screen`、`nohup`等命令的使用方法和案例。
99 4
linux进程管理万字详解!!!
|
17天前
|
存储 运维 监控
深入Linux基础:文件系统与进程管理详解
深入Linux基础:文件系统与进程管理详解
58 8
|
14天前
|
Linux
如何在 Linux 系统中查看进程占用的内存?
如何在 Linux 系统中查看进程占用的内存?
|
26天前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
66 4
|
27天前
|
缓存 负载均衡 算法
Linux内核中的进程调度算法解析####
本文深入探讨了Linux操作系统核心组件之一——进程调度器,着重分析了其采用的CFS(完全公平调度器)算法。不同于传统摘要对研究背景、方法、结果和结论的概述,本文摘要将直接揭示CFS算法的核心优势及其在现代多核处理器环境下如何实现高效、公平的资源分配,同时简要提及该算法如何优化系统响应时间和吞吐量,为读者快速构建对Linux进程调度机制的认知框架。 ####
|
28天前
|
消息中间件 存储 Linux
|
2月前
|
运维 Linux
Linux查找占用的端口,并杀死进程的简单方法
通过上述步骤和命令,您能够迅速识别并根据实际情况管理Linux系统中占用特定端口的进程。为了获得更全面的服务器管理技巧和解决方案,提供了丰富的资源和专业服务,是您提升运维技能的理想选择。
50 1
|
2月前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
【10月更文挑战第9天】本文将深入浅出地介绍Linux系统中的进程管理机制,包括进程的概念、状态、调度以及如何在Linux环境下进行进程控制。我们将通过直观的语言和生动的比喻,让读者轻松掌握这一核心概念。文章不仅适合初学者构建基础,也能帮助有经验的用户加深对进程管理的理解。
27 1