linux 进程间通信(一、总论及管道)

1、背景

1.1 参考资料

https://blog.csdn.net/maopig/article/details/77800124

 

2、进程间通信

每个进程有独立的地址空间,任何一个进程的全局变量在另一个进程中都是看不见的。因此进程间要交换数据必须通过内核在内核中开辟一块缓冲区,进程1把数据从用户空间拷贝到内存缓冲区,进程2再从内核缓冲区把数据读走。内核提供的这种机制称为进程间通信(IPC, inter process communication)。

linux 进程间通信(一、总论及管道)_第1张图片

对于32位Linux内核地址空间划分0~3G为用户空间,3~4G为内核空间。逻辑地址。

3、管道

3.1 匿名管道缓冲区的实现机制

管道是最基本的IPC机制。在内核中开辟一块缓冲区(称为管道 pipe),提供一个读端文件描述符和一个写端文件描述符,供用户通信。

管道是一种最基本的IPC机制,作用于有亲缘关系的进程之间,完成数据传递。调用pipe系统函数即可创建一个管道,有如下特质:

1、其本质是一个伪文件(实为内核缓冲区);[伪文件类型有:-s 套接字、-b 块设备、-c 字符设备、 -p 管道; 占磁盘空间的文件类型有: - 普通文件、 -d 目录、 -l 软连接]

2、由两个文件描述符引用,一个表示读端,一个表示写端;

3、规定数据从管道的写端流入管道,从读端流出,有FIFO的特性。

管道的原理:管道实为内核使用环形队列机制,借助内核缓冲区(4K = 8*512bytes; 512是磁盘一个扇区的大小)实现的。

 

管道局限性:

1> 数据不能自己读 自己写

2> 数据一旦被读走,便不再管道中存在,即不可反复读取

3> 由于管道采用半双工通信方式,因此数据只能在一个方向上流动

4》只能在有公共祖先的进程间使用管道

常见的通信方式有:单工通信、半双工通信、全双工通信

双工:有两个工作模式----接收和发送

单工:只有一个工作模式---接收或发送

半双工:用的时候,只能选择接收或发送一个模式

全双工:用的时候,同时可以接收和发送。

在linux中,管道的实现并没有专门的数据结构,而是借助了文件系统的file结构和VFS的索引节点inode。通过将两个file结构指向同一个临时的VFS索引节点,而这个索引节点又指向一个物理页面而实现的。

linux 进程间通信(一、总论及管道)_第2张图片

所上图所示,两个file数据结构定义文件操作例程地址(f_op)是不同的,其中一个是向管道写入数据的例程地址,而另一个是从管道读取数据的例程地址。就这样,用户程序就可以对管道的两个文件描述符分别进行read、write系统调用(表现的依旧是文件操作),而内核却利用这种抽象机制实现了管道这一特殊操作。普通管道仅供具有共同祖先的两个进程之间共享,并且这个祖先已经为他们建立了供他们使用的管道。因为文件描述符是针对进程的。

3.2 匿名管道这个缓冲区有多大

内核给我们创建的管道的buffer有多大,是由pipe buf 和缓冲条目的数量共同决定的。pipe buf *条数 == pipe capactiy;

pipe buf定义了内核管道缓存区的容量,这个值由内核设定,可以通过ulimit-a命令查看

linux 进程间通信(一、总论及管道)_第3张图片

从查看到的资源看pipe size 512bytes * 8 = 4096bytes。即一次原子写入为4096字节。

当然另一个与之对应的缓冲条目的个数与linux的内核版本是有关联的,16条;4096*16 = 65536字节。

当管道满时,

对于O_NONBLOCK disable的:write调用阻塞,知道有程序读走数据;

对于O_NONBLOCK enable的:write调用返回-1,errno值为EAGAIN。

把管道写满需要多少字符。经验是65536个。

3.3 匿名管道的实际应用

头文件:#include

原型: int pipe(int fd[2]);

函数说明: pipe()会建立管道,并将文件描述符由参数fl数组返回。fd[0]为管道的读取端,fd[1]是管道的写入端。

返回值: 成功 0  失败 -1, 错误原因存于errno中

错误代码:

EMFILE-----进程已用完文件描述符最大量;

ENFILE-----系统已无文件描述符可用;

EFAULT-----参数fd数组地址非法 

 开辟管道之后,父子进程是如何实现通信的,如下图所示步骤通信

linux 进程间通信(一、总论及管道)_第4张图片

1、父进程调用pipe开辟管道,得到连个文件描述符指向管道两端;

2、父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一个管道;

3、父进程关闭管道读端,子进程关闭写端,这样就可以实现父进程写,子进程读的进程间通信了。

即读的进程,要关闭fd[1];   写的进程要关闭fd[0];

3.4 匿名管道在读写上的操作

3.4.1读管道时

1>当管道中有数据,则read返回实际读到的字节数;

2>当管道中没有数据时

        2.1》写端全关闭,read返回0

        2.2》仍有写段打开,则阻塞等待    这就是为啥例程中,父进程早于子进程结束。

3.4.2 写管道时

1>当读端全关闭,则进程异常终止(SIGPIPE信号)

2>当仍有读端打开

    2.1》当管道未满,写入数据,返回写入的字节数

    2.2》当管道满时,阻塞

 

3.5匿名管道的限制和优点

限制:

1、匿名管道只能单向通信,要实现双向通信,要再创建一个管道;

2、管道通信依赖于文件系统,即管道的生命周期随进程。

3、匿名管脚只能进行在有亲缘关系的进程间通信,通常用于父子进程、兄弟进程;

4、管道容量有限

优点:

某些限制,从其他角度看也是优点。

1、自带同步机制,保证读写顺序一致------FIFO先进先出

2、管道的通信被称为面向字节流,与通信格式无关。

3、单向通信

3.5 Linux 下标准C库的匿名管道的函数和应用

在应用匿名管道进行编程时,基本的流程是一样的:先创建一管道,然后并发一个进程,父子进程间通过管道进行数据交换。

头文件:

函数原型: FILE *popen(const char *command, const char *type);

参数: command----要执行的命令串;

type---指定要返回的FILE 类型指针是用于读,还是写的

返回值: NULL---执行失败,可用errno查错误原因。

其他:就是一个标准I/O流。

通过fgets()来获取执行输出。

下面举个栗子

#include 
#include 
#include 
#include 
#define BUFFER_SIZE        (1024)


int execute_shell_command(const char *cmd, char * result)
{
    if((cmd == NULL) || (result == NULL))
        return -1;
 
   FILE * stream = NULL;
   if((stream = popen(cmd, "r")) == NULL)
   {
         perror("popen");
   }
   while(fget(result, BUFFER_SIZE, stream) != NULL)
   {
        pclose(stream);
        stream = NULL;
        return 0;
   }       
    
}


void main(void)
{

    char * command = "pwd 2>/dev/null; echo $?";
    char result[BUFFER_SIZE] = {0};
    if(execute_shell_command(command, result)!=-1)
    {
          printf("reslut = %d\n", result);      
    }
    else
    {
         printf("execute shell %s failed\n", command);
    }
    
    
}

 

3.6 Linux 下有名管道

有名管道可以在不具有亲缘关系的进程间实现数据通信。与匿名管道不同,有名管道可通过路径名指出,并且在文件系统中是可见的。匿名管道文件描述符指向的文件(缓冲区)是在内存上的。

有名管道FIFO(First in first out)是特殊的文件,它是严格地遵循先进先出规则,即对管道和FIFO的读总是从开始出返回数据,对它们的写则是把数据添加到末尾。注意不支持lseek等文件定位操作。

有名管道之所以可以实现进程间的通信在于通过同一路径名,可以看到同一份资源,这份资源以FIFO的文件形式存在于文件系统中

在创建管道成功之后,就可以使用open、read、write这些函数了。

创建有名管道----在文件系统中创建一个专用文件,该文件用于提供FIFO功能。

头文件:#include

          #include

函数原型: int mkfifo(const char * filename, mode_t mode);

参数: filename-----指定了文件名

         mode-----指定了新建文件的访问权限,与文件的访问权限相同,如0644.

O_RDONLY   读管道

O_WRONLY  写管道

O_RDWR       读写管道

O_NONBLOCK    非阻塞

O_CREAT             如文件不存在,则创建一个新的文件,并用第三的参数为其设置权限。

O_EXCL            如使用O_CREAT时文件存在,那么可返回错误信息。这一参数可测试文件是否存在。

返回值: 0 函数执行成功; -1:执行失败,可查询errno获取错误详细信息。

创建一个名为pathname的文件系统节点(文件、设备文件、有名管道),其属性有mode和dev指定。

头文件:#include

#include

#include

#include

函数原型:  int mknod(const char * pathname, mode_t mode, dev_t dev);

pathname:

mode: 指定要用到的权限和创建的节点的类型。

权限:(mode&(~umask))

类型: S_IFREG, S_IFCHR, S_IFBLK, S_FIFO 或S_IFSOCK,以分别指定普通文件(创建后为空),字符特殊文件、块特殊文件、FIFO(有名管道)、UNIX域套接字。

dev:

mknod是比较老的函数,而mkfifo函数更加简单和规范。尽量使用mkfifo而不是mknod.

 

需要注意的是:有名管道FIFO这一专用文件,读写打开方式有所不同,有两种方式:阻塞方式与非阻塞方式。在缺省状态下,open打开的是以阻塞方式打开。

在非阻塞方式下: 只读方式打开管道,立即返回。以写方式打开管道,若之前没有以读方式打开管道的进程,则返回失败,错误信息为ENXIO。

在阻塞方式下:当打开一个管道用于读时,如果没有打开管道用于写的进程,则该打开操作将被阻塞,直到有一个进程用写的方式打开该管道为止。同样,先打开写,也要等有进程打开读。 

具体使用如下所示:

下面以阻塞的方式打开匿名管道,看看读写的阻塞情况。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#define FIFO   "/tmp/myfifo"

int main(void)
{
    char* buf = "I am jimmy.";
    int fd;
    if((mkfifo(FIFO, O_CREAT|O_EXCL) < 0)&&(errno != EEXIST))  /*创建有名管道,访问权限为644*/
    {
         printf("cannot crate fifoserver\n");      /*错误处理*/
                    
    }
    printf("Preparing for reading bytes...\n");
    if(fd = open("p1", O_WRONLY, 0) < 0)
    {
         perror("open");
         exit(-1);
    }
    write(fd, buf, strlen(buf));
     
    
    close(fd);
    sleep(5);   
    unlink(FIFO);
    return 0;
}


int main(void)
{
    char buf[1024] = {0};
    int fd;
   
    if(fd = open("p1", O_RDONLY, 0) < 0)
    {
         perror("open");
         exit(-1);
    }
   if( read(fd, buf, 1024) > 0)
  {
      printf("read %s\n",buf); 
  }
   
    close(fd);
  
    return 0;
}

 

你可能感兴趣的:(linux 进程间通信(一、总论及管道))