zoukankan      html  css  js  c++  java
  • linux 之进程间通信-------------InterProcess Communication

      进程间通信至少可以通过传送打开文件来实现,不同的进程通过一个或多个文件来传递信息,事实上,在很多应用系统里,都使用了这种方法。但一般说来,进程间 通信(IPC:InterProcess Communication)不包括这种似乎比较低级的通信方法。Unix系统中实现进程间通信的方法很多,而且不幸的是,极少方法能在所有的Unix系 统中进行移植(唯一一种是半双工的管道,这也是最原始的一种通信方式)。而Linux作为一种新兴的操作系统,几乎支持所有的Unix下常用的进程间通信 方法:管道、消息队列、共享内存、信号量、套接口等等。

      1、管道(pipe)

      管道是进程间通信中最古老的方式,它包括无名管道和有名管道两种,

        无名管道用于父进程和子进程间的通信,

        有名管道用于运行于同一台机器上的任意两个进程间的通信。

      无名管道由pipe()函数创建:

        #include  <unistd.h>

        int pipe(int  filedis[2]);

      参数filedis返回两个文件描述符:filedes[0]为读而打开,filedes[1]为写而打开;filedes[1]的输出是filedes[0]的输入

     1  1 #include <stdio.h>
     2  2 #include <string.h>
     3  3 #include <stdlib.h>
     4  4 #include <errno.h>
     5  5 #include <unistd.h>
     6  6 
     7  7 #define INPUT 0
     8  8 #define OUTPUT 1
     9  9 
    10 10 int main()
    11 11 {
    12 12     int file_descriptors[2];
    13 13     pid_t pid;
    14 14     char buf[256];
    15 15     char send1[256];
    16 16     int returned_count;
    17 17     
    18 18     //创建无名管道
    19 19     pipe(file_descriptors);
    20 20     //创建子进程
    21 21     if((pid=fork()) == -1)
    22 22     {
    23 23         printf("Error in fork
    ");
    24 24         exit(1);
    25 25     }
    26 26     //执行子进程
    27 27     if(pid == 0)
    28 28     {
    29 29         printf("int the child process....
    ");
    30 30         fgets(send1,256, stdin);
    31 31 
    32 32         //子进程向父进程写数据,关闭管道的读端;
    33 33         close(file_descriptors[INPUT]);
    34 34         write(file_descriptors[OUTPUT], send1, strlen(send1));
    35 35         exit(0);
    36 36     }
    37 37     else //执行父进程
    38 38     {
    39 39         printf("int the parent process...
    ");
    40 40         //父进程从管道读取子进程写的数据,关闭管道的写端
    41 41         close(file_descriptors[OUTPUT]);
    42 42         returned_count = read(file_descriptors[INPUT], buf, sizeof(buf));
    43 43         printf("%d bytest of data received from child process: %s
    "
    44 44                 ,returned_count,buf);
    45 45     }
    46 46 
    47 47     return 0;
    48 48 }
    49 ////////////////////////
    50 [root@cp ~]# ./a.out
    51 int the parent process...
    52 int the child process....
    53 lkfjasldkf
    54 11 bytest of data received from child process: lkfjasldkf

      在linux系统下,有名管道可由两种方式创建:命令行方式mknod  系统调用 和函数mkfifo。

      生成了有名管道后,就可以使用一般的文件I/O函数如open, close, read, write等来对它进行操作。

    什么是命名管道称为FIFO文件,它是一种特殊类型的文件,它在文件系统中以文件名的形式存在;

      由于Linux中所有的事物都可被视为文件,所以对命名管道的使用也就变得与文件操作非常统一,也使得它的使用非常方便,同时我们也可以像平常的文件名一样在命令中使用。

      使用下面两个函数均可以创建一个命名管道,函数原型如下:

      #include   <sys/types.h>

      #include  <sys/stat.h>

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

      int mknod(const  char *filename,   mode_t  mode  | S_IFIFO,   (dev_t)0);

    这两个函数都能创建一个FIFO文件,注意是创建一个真实存在于文件系统中的文件,filename指定了文件名,而mode则指定了文件的读写权限;

      mknod 是比较老的函数,而使用mkfifo函数更加简单和规范,所以建议在可能的情况下,尽量使用mkfifo而不是mknod。

    访问命名管道

      1)打开FIFO文件

        与打开其他文件一样,FIFO文件也可以使用open调用来打开。注意,mkfifo函数仅仅只是创建一个FIFO文件,要使用命名管道还要将其打开。

      注意点:

        1、程序不能以O_RDWR模式打开FIFO文件进行读写操作,而其行为没有明确定义。因为假如一个管道以读/写方式打开,进程就会读回自己的输出,同时我们通常使用FIFO只是为了单向的数据传递

        2、传递给open调用的是FIFO的路径名,而不是正常的文件。

    打开FIFO文件通常有四种方式

      open(const  char *path,  O_RDONLY);   //只读  并默认是阻塞

      open(const  char  *path,  O_RDONLY | O_NOBLOCK);  //无阻塞只读

      open(const  char  *path,   O_WRONLY);   //只写 并默认是阻塞

      open(const  char  *path,    O_WRONLY | O_NONBLOCK);  //无阻塞只写

    open调用的阻塞是怎么一回事? 很简单,对于只读(O_RDONLY)打开的FIFO文件,如果open调用是阻塞的,除非有一个进程以写方式打开同一个FIFO, 否则它读不到数据就不会返回; 如果open调用是非阻塞的,则即使没有其他进程以写方式打开同一FIFO方式,open调用将成功并立即返回。

      对于以只写方式(O_WRONLY)打开的FIFO文件,如果open调用是阻塞的(即第二个参数为O_WRONLY),open调用将被阻塞,直到有一 个进程以只读方式打开同一个FIFO文件为止;如果open调用是非阻塞的(即第二个参数为O_WRONLY | O_NONBLOCK),open总会立即返回,但如果没有其他进程以只读方式打开同一个FIFO文件,open调用将返回-1,并且FIFO也不会被打 开。

      使用FIFO实现进程间的通信

     两个进程如何通过FIFO实习通信,首先要有两个进程,一个源文件为fifowrite.c,  它在需要时创建管道,然后向管道写入数据,数据有文件Data.txt提供,大小为10M, 内容都是字符‘0’, 另一个源文件为fiforead.c,它从FIFO中读取数据,并把读到的数据保存到另一个文件DataFormFIFO.txt中,

      命名管道的安全问题

      前面指的是两个进程之间通信问题,也就是说,一个进程向FIFO文件写数据,而另一个进程从FIFO文件中读取数据。

      假如只使用一个FIFO文件,如果有多个进程同时向同一个FIFO文件写数据,而只有一个读FIFO进程在同一个FIFO文件中读取数据时,则会发生数据块的相互交错问题。而在实际中多个不同进程向一个FIFO读进程发送数据是很常见的;

      为了解决这一问题,就是让写操作的原子化。即: 系统规定:在一个以O_WRONLY(阻塞方式)打开的FIFO中,如果写入的数据长度小于等待PIPE_BUF,  结果是 要么写入全部字节, 要么 一个字节都不写入。    如果所有的写请求都是发往一个阻塞的FIFO时,并且每个写请求的数据长度小于等于PIPE_BUF字节,系统就可以确保数据决不会交错在一起;

      为了数据的安全,我们很多时候要采用阻塞的FIFO,让写操作变成原子操作。

      //fiforead.c
    1
    #include <unistd.h> 2 #include <stdlib.h> 3 #include <stdio.h> 4 #include <fcntl.h> 5 #include <sys/types.h> 6 #include <limits.h> 7 #include <string.h> 8 9 int main() 10 { 11 int pipe_fd = -1; 12 int data_fd = -1; 13 const char *fifo_name = "./my_fifo"; 14 int res = 0; 15 int open_mode = O_RDONLY; 16 char buffer[PIPE_BUF + 1]; 17 int bytes_read = 0; 18 int bytes_write = 0; 19 20 memset(buffer, '', sizeof(buffer)); 21 printf("Process %d opening FIFOO O_RDONLY ", getpid()); 22 23 //以只读阻塞方式打开管道文件, 名字必须与fifowrite.c文件中的FIFO同名 24 pipe_fd = open(fifo_name, open_mode); 25 26 //以只写方式创建保存数据的文件 27 data_fd = open("DataFormFIFO.txt", O_WRONLY|O_CREAT, 0644); 28 printf("Process %d result %d ", getpid(), pipe_fd); 29 30 if(pipe_fd != -1) 31 { 32 do 33 { 34 //读取FIFO中的数据,并把它保存在文件DataFormFIFO.txt文件中 35 res = read(pipe_fd, buffer, PIPE_BUF); 36 bytes_write = write(data_fd, buffer, res); 37 bytes_read += res; 38 }while(res >0); 39 close(pipe_fd); 40 close(data_fd); 41 } 42 else 43 { 44 exit(EXIT_FAILURE); 45 } 46 printf("Process %d finished, %d bytes read ", getpid(), bytes_read); 47 exit(EXIT_SUCCESS); 48 return 0; 49 }
    [root@cp pipe]# gcc -o write fifowrite.c
    [root@cp pipe]# ./write
    //该线程处于阻塞状态;

     1 #include <unistd.h>
     2 #include <stdlib.h>
     3 #include <stdio.h>
     4 #include <fcntl.h>
     5 #include <sys/types.h>
     6 #include <limits.h>
     7 #include <string.h>
     8 
     9 int main()
    10 {    
    11     int pipe_fd = -1;
    12     int data_fd = -1;
    13     const char *fifo_name = "./my_fifo";
    14     int res = 0;
    15     int open_mode = O_RDONLY;
    16     char buffer[PIPE_BUF + 1];
    17     int bytes_read = 0;
    18     int bytes_write = 0;
    19     
    20     memset(buffer, '', sizeof(buffer));
    21     printf("Process %d opening FIFOO O_RDONLY 
    ", getpid());
    22     
    23     //以只读阻塞方式打开管道文件, 名字必须与fifowrite.c文件中的FIFO同名
    24     pipe_fd = open(fifo_name, open_mode);
    25 
    26     //以只写方式创建保存数据的文件
    27     data_fd = open("DataFormFIFO.txt", O_WRONLY|O_CREAT, 0644);
    28     printf("Process %d result %d
    ", getpid(), pipe_fd);
    29         
    30     if(pipe_fd != -1)
    31     {
    32         do
    33         {
    34             //读取FIFO中的数据,并把它保存在文件DataFormFIFO.txt文件中
    35             res = read(pipe_fd, buffer, PIPE_BUF);
    36             bytes_write = write(data_fd, buffer, res);
    37             bytes_read += res;    
    38           }while(res >0);
    39         close(pipe_fd);
    40         close(data_fd);
    41     }
    42     else
    43     {
    44         exit(EXIT_FAILURE);
    45     }
    46     printf("Process %d finished, %d bytes read 
    ", getpid(), bytes_read);
    47     exit(EXIT_SUCCESS);
    48     return 0;
    49 }
    //[root@cp pipe]# gcc -o write fifowrite.c
    [root@cp pipe]# ./write

    2、消息队列

      

    3、共享内存

      共享内存是运行在同一台机器上的进程间通信最快的方式,因为数据不需要在不同的进程间复制。通常由一个进程创建一块共享内存区,其余进程对这块内存区进行读写。得到共享内存有两种方式:映射/dev/mem设备和内存映像文件。前一种方式不给系统带来额外的开销,但现实中并不常用,因为它控制存取的将是实际的物理内存,在linux系统下,这只有通过限制linux系统存取的内存才可以做到,这当然不太实际。常用的方式是通过shmxxx函数族来实现利用共享内存进行存储的。

      首先是用的函数是shmget,它获得一个共享存储标识符。

      #include <sys/types.h>

      #include <sys/ipc.h>

      #include <sys/shm.h>

      int shmget(key_t  key, int  size,  int flag);

      这个函数有点类似malloc函数,系统按照请求分配size大小的内存用作共享内存。linux系统内核中每个IPC结构都有一个非负整数的标识符,这样对一个消息队列发送消息时总要引用标识符就可以了。这个标识符是内核由IPC结构的关键字得到的,这个关键字,就是上面第一个函数的key。 数据类型key_t是头文件sys/types.h 中定义的,它是一个长整形的数据。

      当共享内存创建后,其余进程可以调用shmat() 将其连接到自身的地址空间中;

      void  *shmat(int  shmid, void *addr,  int  flag);

      shmid 为shmget函数返回的共享存储标识符, addr 和flag参数决定了以什么方式来确定连接的地址,函数的返回值即是该进程数据段所连接的实际地址, 进程可以对此进程进行读写操作;

      使用共享存储来实现进程间通信的注意点是对数据存取的同步,必须确保当一个进程去读取数据时,它所想要的数据已经写好了。通常,信号量被要来实现对共享存储数据存取的同步,另外,可以通过使用shmctl函数设置共享存储内存的某些标志位如SHM_LOCK,

    SHM_UNLOCK等来实现。

    4、信号量

      为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域。临界区域是指执行数据更新的代码需要独占式地执行。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它,也就是说信号量是用来协调进程对共享资源的访问的。

      信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P(信号量)) 和  发送 (即V(信号变量))操作。最简单的信号量是只能取0和1的变量,这也是信号量最常见的一种形式,叫做二进制信号量。 还有一种可以取多个正整数的信号量称为通用信号量,下面主要讨论二进制信号量;

      信号量的工作原理

      由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:

        P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行。

        V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1;

    就是两个进程共享信号量sv,

      一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,使sv减1。

      而第二个进程将被阻止进入临界区, 因为当它试图执行P(sv)时,sv为0,它会被挂起以等待第一个进程离开临界区域并执行V(sv)释放信号量,这时第二个进程就可以恢复执行。  

      Linux的信号量机制

    Linux提供了一组精心设计的信号量接口来对信号进行操作,它们不只是针对二进制信号量,下面将会对这些函数进行介绍,但请注意,这些函数都是用来对成组的信号量值进行操作的。它们声明在头文件sys/sem.h中。
      1、semget函数
      它的作用是创建一个新信号量或取得一个已有的信号量,原型为:
        int  semget(key_t key, int num_sems,  int sem_flags);
      参数说明:
        第一个参数:key是整数值(唯一非零),不相关的进程可以通过它访问一个信号量,它代表程序可能要使用的某个资源,程序对所有信号量的访问都是间接的,
        程序先通过调用semget函数并提供一个键,再由系统生成一个相应的信号标识符(semget函数的返回值),只有semget函数才直接使用信号量键;所有其他的信号量函数使用由semget函数返回的信号量标识符,如果多个程序使用相同的key值,key将负责协调工作;
      第二个参数:num_sems指定需要的信号量数目,它的值几乎总是1;
      第三个参数:sem_flags 是一组标志,当想要在信号量不存在的情况时则创建一个新的信号量,可以和值IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。而IPC_CREAT|IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号已存在,返回一个错误;
      semget函数成功返回一个相应信号标识符(非零),失败返回-1;
    semop函数
      功能:改变信号量的值,原型为:
      int  semop(int sem_id, struct sembuf *sem_opa, size_t  num_sem_ops);
      参数说明:
        第一个参数:sem_id是由semget返回的信号量标识符,
        sembuf结构定义如下:
          struct sembuf{
            short sem_num;  //除非使用一组信号量,否则它为0
            short sem_op;   //信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,P(等待)操作
                      //一个是+1,即V(发送信号)操作。              
               short sem_flg;//通常为SEM_UNDO,使操作系统跟踪信号,
                   //并在进程没有释放该信号量而终止时,操作系统释放信号量
          };
     

      信号量称为信号灯,它是用来协调不同进程间的数据对象的,而最主要的应用是前一节的共享内存方式的进程间通信。本质上,信号量是一个计数器,它用来记录对某个资源(如共享内存)的存取状况,一般来说,为了获得共享资源,进程需要执行下列操作:

      1、测试控制该资源的信号量

      2、若此信号量的值为正,则允许进行使用该资源。进程将信号量减1

      3、若此信号量为0,则该资源目前不可用,进程进入睡眠状态,直到信号量大于0.进程被唤醒,转入步骤1;

      4、当进程不再使用一个信号量控制的资源时,信号量加1。如果当时有进程正在睡眠等待此信号量,则唤醒此进程。

    维护信号量状态的是Linux内核操作系统而不是用户进程。我们可以从头文件/usr/scr/linux/include/linux/sem.h中看到内核用来维护信号量状态的各个结构的定义。信号量是一个数据集合,用户可以单独使用这一集合的每个元素。要调用的第一个函数是semget,用以获得一个信号量ID;

      

      

      

  • 相关阅读:
    在js中如何将字符串类型的日期("2020-11-30T02:21:42.000+0000")进行格式化
    微信小程序:报错fail webview count limit exceed
    微信小程序:picker组件实现下拉框效果
    微信小程序:post请求参数放在请求体中还是拼接到URL中需要看后台是如何接收的
    【华为云技术分享】如何用交互式特征工程工具进行数据分析处理
    【云小课】基础服务第25课 容灾演练:平时多练兵,急时保可用!
    【华为云分享】软件工程的迷途与沉思
    WebSocket 从入门到写出开源库
    教你轻松截获 Selenium 中的 Ajax 数据
    【华为云技术分享】Scrum Master如何引导团队中的刺头
  • 原文地址:https://www.cnblogs.com/chris-cp/p/3532216.html
Copyright © 2011-2022 走看看