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* 之间的的关系。


目录
相关文章
|
6天前
|
消息中间件 算法 Linux
【Linux】详解如何利用共享内存实现进程间通信
【Linux】详解如何利用共享内存实现进程间通信
|
6天前
|
Linux
【Linux】命名管道的创建方法&&基于命名管道的两个进程通信的实现
【Linux】命名管道的创建方法&&基于命名管道的两个进程通信的实现
|
6天前
|
Linux
【Linux】匿名管道实现简单进程池
【Linux】匿名管道实现简单进程池
|
6天前
|
Linux
【Linux】进程通信之匿名管道通信
【Linux】进程通信之匿名管道通信
|
6天前
|
存储 Linux Shell
Linux:进程等待 & 进程替换
Linux:进程等待 & 进程替换
30 9
|
6天前
|
存储 Linux C语言
Linux:进程创建 & 进程终止
Linux:进程创建 & 进程终止
29 6
|
6天前
|
Linux 数据库
linux守护进程介绍 | Linux的热拔插UDEV机制
linux守护进程介绍 | Linux的热拔插UDEV机制
linux守护进程介绍 | Linux的热拔插UDEV机制
|
6天前
|
Unix Linux 调度
linux线程与进程的区别及线程的优势
linux线程与进程的区别及线程的优势
|
6天前
|
Linux 调度 C语言