首页 > 嵌入式 > 系统编程 > 进程间通信-管道、共享内存
2018
05-01

进程间通信-管道、共享内存

管道

概述

当从一个进程连接数据流到另一个进程时,我们称之为管道(pipe)。通常是把一个进程的输出通过管道连接到另一个进程的输入。比如在命令行执行如下的操作时,实际上就是把一个进程的输出直接传递给另一个进程的输入。
cmd1 | cmd2

shell负责安排两个命令的标准输入和标准输出。以上形式的调用,cmd1的标准输出来自终端键盘,cmd1的标准输出传递给cmd2,作为它的标准输入,cmd2的标准输出连接到终端屏幕,在屏幕上打印cmd2的执行结果。shell所做的工上实际上是对两个程序的标准输入和标准输出进行了重新连接,使用数据流从键盘输入后通过两个命令后最终输出到屏幕上。
示倒:

ps -ax | grep process #通过进程名process称查找进程对应的PID

进程间通信-管道、共享内存 - 第1张  | rs232的博客

进程管道

可能是在两个程序之间传递数据最简单的方法,使用popen和pclose函数。函数原型如下:

#include <stdio.h>
FILE *popen(const char *command, const char *type);

描述:
打开一个命令,对其传递数据,或是接收它的命令输出。
参数:
command:要打开的命令名称和参数
type:打开方式,只能是”r”或”w”,使用”r”时,调用方可以通过返回的文件指针读取命令的输出,使用fread来操作。如果是”w”,则调用方可以向命令写入数据,通过fwrite来完成。只能取”r”或”w”,意味着程序只能读或写管道,而不能同时读写。
返回值:
成功返回文件指针,失败返回NULL。

int pclose(FILE *stream);

描述:
关闭与管道相关的文件指针,只在popen启动的进程结束后才返回,如果调用pclose时它还在运行,则pclose将等待该进程结束。pclose调用返回调用进程的退出码,如果在调用pclose之前执行了一个wait语句,则调用进程的退出码将丢失,pclose返回-1并设置errno为ECHILD。

示例:uname.c

#include "common.h"
#define BUFSIZE 4096
int main(void)
{
FILE *fp = NULL;
char buffer[BUFSIZE + 1] = {0};
fp = popen("uname -a", "r");
if(fp == NULL)
err_sys("popen error");
else
{
if(fread(buffer, 1, BUFSIZE, fp) > 0)
printf("Output is:\n%s\n", buffer);
}
pclose(fp);
return 0;
}

示例:wc.c

#include "common.h"
#define BUFSIZE 4096
int main(void)
{
FILE *fp = NULL;
char buffer[BUFSIZE + 1] = {0};
fp = popen("wc -l", "w");
if(fp == NULL)
err_sys("popen error");
else
{
sprintf(buffer, "hehe\nhehehe\nheheheh\nhehehe\n");
fwrite(buffer, 1, strlen(buffer), fp);
}
pclose(fp);
return 0;
}

管道(pipe)

又称无名管道,通过pipe函数来实现。这是一个底层的系统调用,通过它在两个进程间传递数据不需要启动shell。同时它还提供对读写数据更多的控制。

管道是一种特殊类型的文件,在应用层体现只有两个打开的文件描述符,而在文件系统中没有文件与之对应,所有的文件数据都存在内存中。这两个文件描述符一个用于读,一个用于写,数据只能从管道的一端写入,从另一端读出,写入管道中的数据遵循先入先出的规则。数据一旦从管道中读走,它在管道中就被抛弃,释放空间以便于写入更多的数据。对管道的读取与写入可以简单地通过read与write来完成。

#include <unistd.h>
int pipe(int fd[2]);

描述:
创建管道,返回管道读取端和写入端的文件描述符
参数:
fd[2],作输出参数,用于存储创建好后的读取端和写入端的文件描述符。
返回值:
成功返回0,失败返回-1。

两个返回的文件描述符由系统进行连接,所有写到fd[1]的数据都可以从fd[0]中读回来。数据基于先进先出的原则进行处理。

进程间通信-管道、共享内存 - 第2张  | rs232的博客

示例:pipe1.c

#include "common.h"
int main(void)
{
int fd[2];
int n;
char data[1024] = "123456";
if(pipe(fd) < 0)
err_sys("pipe error");
n = write(fd[1], data, strlen(data));
printf("write %d bytes\n", n);
n = read(fd[0], data, 1024);
printf("read %d bytes:%s\n", n, data);
exit(0);
}

借助fork的方式创建进程,可以把管道用于父子进程间的通信。如下图所示,打开的管道文件描述符可以被子进程复制,则父子进程可以通过管道进行数据传输。

进程间通信-管道、共享内存 - 第3张  | rs232的博客

当把管道用于进程间通信时,要注意一下管道读写数据的特点,如下:

  1. 默认用read函数从管道中读数据是阻塞的,即没有数据写入管道时,read操作会阻塞。
  2. 调用write函数向管道里写数据,当缓冲区已满时write也会阻塞 。
  3. 通信过程中,读端口全部关闭后,写进程向管道内写数据时,写进程会(收到SIGPIPE信号)退出。
  4. 编程时可通过fcntl函数设置文件的阻塞特性。
    设置为阻塞:fcntl(fd, F_SETFL, 0);
    设置为非阻塞:fcntl(fd, F_SETFL, O_NOBLOCK);

示例:pipe2.c

#include "common.h"
int main(void)
{
int fd[2];
pid_t pid;
if(pipe(fd) < 0)
err_sys("pipe error");
pid = fork();
if(pid < 0)
err_sys("fork error");
else if(pid == 0)
{
close(fd[0]);
int n;
char data[] = "123456";
sleep(1);
n = write(fd[1], data, strlen(data));
printf("child write %d bytes\n", n);
exit(0);
}
else
{
close(fd[1]);
int n;
char data[1024];
printf("parent read start...\n");
n = read(fd[0], data, 1024);
printf("parent read %d bytes:%s\n", n, data);
exit(0);
}
return 0;
}

关于管道关闭后的读操作

默认用read函数从管道中读数据是阻塞的,它会在以下两种情况下返回,一是管道中有数据到达,即向写描述符写入了数据,二是管道的写入端被关闭,此时read返回0,类似于读到了文件结尾。但是在使用fork之后,父子进程都有管道的写入端文件描述符,一个在父进程,一个在子进程,则只有在父子进程中都把写描述符关闭后,管道才会被认为是关闭,对管道的read操作才会失败,所以,如果要通过fork的方式使用管道进行父子进程的通信,则一定要注意父子进程只保留各自要操作的描述符,把不需要的文件描述符关闭。

关于fcntl函数

fcntl系统调用对文件描述符提供了更多的操纵方法,比如对它们进行复制、获取和设置文件描述符标志、获取和设置文件状态标志,以及管理建议性文件锁等。

#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);

参数fd表示要操作的文件描述符,cmd表示操作的命令参数,根据命令的不同,可能还需要第三个参数arg。常用的操作命令有以下以几个:

  • fcntl(fd, F_DUPFD, newfd):复制文件描述符fd,返回一个新的文件描述符,其数值等于或大于newfd。
  • fcntl(fd, F_GETFD):获取文件描述符标志。比如FD_CLOEXEC标志,它的作用时决定是否在成功调用了某个exec系列的系统调用之后关闭该文件描述符。
  • fcntl(fd, F_SETFD, flags):设置文件描述符标志,通常仅用来设置FD_CLOEXEC。
  • fcntl(fd, F_SETFL)和fcntl(fd, F_GETFL, flags):获取或设置文件状态标志和访问模式。比如打开文件时指定的访问方式(O_RDONLY、O_RDWR、O_APPEND等)。

exec前后文件描述符的特点:
FD_CLOEXEC标志决定了文件描述符在执行exec后文件描述符是否可用。
文件描述符的FD_CLOEXEC标志默认是关闭的,即文件描述符在执行exec后文件描述符是可用的。
若没有设置FD_CLOEXEC标志位,进程中打开的文件描述符,及其相关的设置在exec后 不变,可供新启动的程序使用。

设置FD_CLOEXEC标志位的方法:

int flags;
flags = fcntl(fd, F_GETFD);//获得标志
flags |= FD_CLOEXEC; //打开标志位
flags &= ~FD_CLOEXEC; //关闭标志位
fcntl(fd, F_SETFD, flags);//设置标志

关于O_NONBLOCK文件状态标志:
O_NONBLOCK标志决定了在读取文件时的阻塞特性,当设置该标志时,文件将以非阻塞方式读取,即没有数据时read函数不阻塞,而是直接返回-1,然后设置errno为EAGAIN或是EWOULDBLOCK。
设置阻塞方式:

fcntl(fd, F_SETFL, 0); #设为阻塞
fcntl(fd, F_SETFL, O_NONBLOCK); #设为非阻塞。

非阻塞方式的read调用:

while(1)
{
n = read(fd, buffer, size);
if(n < 0 && errno == EWOULDBLOCK){
//没有数据可读
continue;
}
}

示例:pipe_nonblock.c

命名管道(FIFO)

FIFO提供了一种在不同进程之间使用管道进行通信的办法。之前的管道程序只能在同一进程或是具有共同祖先的进程间进行通信,使用FIFO后,则可以不受此限制。FIFO的基本性质和无名管道是一样的,除了一点,那就是FIFO在文件系统中以文件名的形式存在,使用之前需要先通过系统调用把这个文件创建出来,然后需要通信的程序各自以只读或只写的方式打开该文件进行通信。当使用FIFO的进程退出后,FIFO文件将继续保存在文件系统中以便以后使用。
在linux系统中可以通过mkfifo命令创建一个管道文件,此时,对该文件的读或写就具有了管道的性质,可以用echo和cat命令分别去读或写该文件以验证。

# mkfifo ./myfifo
# cat myfifo

此时,cat命令会阻塞,因为没有程序往这个管道写数据。然后打开一个新的终端,向管道写入数据:

# echo hello > myfifo

此时,cat命令和echo命令都正常返回了。返回的流程是echo先向管道写入数据,然后关闭管道,cat命令从管道中读取数据,因为echo命令已经把管道的写入端关闭了,所以cat命令再次读取管道时会返回0,所以cat命令也退出。

编程接口

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);

功能:
创建一个管道文件
参数:
pathname:管道的路径名+文件名。
mode:mode_t类型的权限描述符,和创建文件时的取值功能一样。
返回值:
成功返回0,如果文件已经存在,则会出错且返回-1,可用access函数检测其是否存在(man 2 access)。
示例:mkfifo.c

读写特点

系统调用的I/O函数都可以作用于FIFO,如open close read write等。所以我们并不需要为读写FIFO数据学习新的系统调用方法。读写FIFO比较麻烦的一点是关于非阻塞标志O_NONBLOCK对打开和读写文件所产生的影响。
程序不能以O_RDWR模式打开FIFO文件进行读写操作,这样做的结果并未明确定义。因为通常使用FIFO只是为了单向传递数据,所以没有必要使用O_RDWR模式。
打开FIFO时,非阻塞标志(O_NONBLOCK)产生以下影响:

特点一:
不指定O_NONBLOCK(即open没有位或O_NONBLOCK)

  1. open以只读方式打开FIFO时,要阻塞到某个进程为写而打开此FIFO
  2. open以只写方式打开FIFO时,要阻塞到某个进程为读而打开此FIFO
    示例:fifo_read_1.c fifo_write_1.c
  3. 调用read函数从FIFO里读数据时如果没有数据到达也会阻塞。
    例:fifo_read_2..c fifo_write_2.c
  4. 通信过程中若写进程先退出了,则调用read函数从FIFO里读数据时不阻塞;若写进程又重新运行,则调用read函数从FIFO里读数据时又恢复阻塞。
    例:fifo_read_3.c fifo_write_3.c
  5. 通信过程中,读进程退出后,写进程向FIFO内写数据时,写进程也会(收到SIGPIPE信号)退出。
    例:fifo_read_4.c fifo_write_4.c
  6. 调用write函数向FIFO里写数据,当缓冲区已满时write也会阻塞。
    打开FIFO时,非阻塞标志(O_NONBLOCK)产生下列影响:

特点二:

  1. 指定O_NONBLOCK(即open位或O_NONBLOCK)
  2. 以只读方式打开:如果没有进程已经为写而打开一个FIFO,只读open成功,并且open不阻塞。
  3. 以只写方式打开:如果没有进程已经为读而打开一个FIFO,只写open将出错返回-1.
  4. read/write从FIFO中读写数据时不阻塞,read在无数据可读时会返回-1。
  5. 通信过程中,读进程退出后,写进程向FIFO内写数据时,写进程也会(收到SIGPIPE信号)退出。
    例:fifo_read_5.c fifo_write_5.c

共享内存

概述

共享内存允许两个或多个进程共享给定的存储区域。它是进程间共享数据最快的一种方法。通过共享内存,进程可以将一段内存连接到自己的存储空间。所有进程都可以访问共享内存中的数据,共享内存中的任何修改,所做的改动将立刻被可以访问该段共享内存的程序看到。
共享内存并未给数据提供同步机制,所以常常需要使用其他机制来同步对共享内存的访问,比如一个进程在向共享内存写数据,则应当使用一种机制保证其他的程序不会同时也去写这些数据,保证数据之间的同步。
共享内存示意图如下:

进程间通信-管道、共享内存 - 第4张  | rs232的博客

系统提供的共享内存有一定的限制,在ubuntu 12.04中共享内存限制值如下:

  1. 共享存储区的最小字节数:1
  2. 共享存储区的最大字节数:32M
  3. 共享存储区的最大个数:4096
  4. 每个进程最多能映射的共享存储区的个数:4096

编程接口

分为四个步骤,对应四个函数,分别是创建->映射->断开->删除。

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

描述:
创建或打开一块共享内存区,与open函数创建或打开文件的行为类似。
参数:
key:IPC键值,key_t类型,用于标识当前系统的这个共享内存,可以手动指定一个整数,也可以通过ftok获得。
size:该共享存储段的长度(字节)
shmflg:标识函数的行为及共享内存的权限,与open函数的第三个参数一样,可以指定以何种权限打开创建或打开共享内存,并且,在共享内存不存在的时候,还可以通过以下两个标志来创建共享内存,共享内存不具有可执行权限。
IPC_CREAT:如果不存在就创建
IPC_EXCL:如果已经存在则返回失败
返回值:
成功:返回共享内存标识符。
失败:返回-1。

使用shell命令操作共享内存:
查看共享内存
ipcs -m
删除共享内存
ipcrm -m shmid

void *shmat(int shmid, const void *shmaddr, int shmflg);

功能:
将一个共享内存段映射到调用进程的数据段中。
参数:
shmid:由shmget返回的共享内存标识符。
shmaddr:共享内存映射地址(若为NULL则由系统自动指定),推荐使用NULL。
shmflg:共享内存段的访问权限和映射条件,可以有以下几种,一般设为0
0:共享内存具有可读可写权限。
SHM_RDONLY:只读。
SHM_RND:(shmaddr非空时才有效)没有指定SHM_RND则此段连接到shmaddr所指定的地址上(shmaddr必需页对齐)。指定了SHM_RND则此段连接到shmaddr – shmaddr%SHMLBA 所表示的地址上。
返回值:
成功:返回共享内存段映射地址
失败:返回 -1
注意:shmat函数使用的时候第二个和第三个参数一般设为NULL和0,即系统自动指定共享内存地址,并且共享内存可读可写。

int shmdt(const void *shmaddr);

功能:
将共享内存和当前进程分离(仅仅是断开联系,并不删除共享内存)。
参数:
shmaddr:共享内存映射地址。
返回值:
成功返回 0,失败返回 -1。

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

功能:共享内存空间的控制。
参数:
shmid:共享内存标识符。
cmd:函数功能的控制,可以有以下几个。
IPC_RMID:删除。
IPC_SET:设置shmid_ds参数。
IPC_STAT:保存shmid_ds参数。
SHM_LOCK:锁定共享内存段(超级用户)。
SHM_UNLOCK:解锁共享内存段。
buf:shmid_ds数据类型的地址,用来存放或修改共享内存的属性,包括共享内存的权限和所有者信息,共享内存在占用了几个段,创建者的PID等信息。
返回值:
成功返回 0,失败返回 -1。
注意:
SHM_LOCK用于锁定内存,禁止内存交换。并不代表共享内存被锁定后禁止其它进程访问。其真正的意义是:被锁定的内存不允许被交换到虚拟内存中。这样做的优势在于让共享内存一直处于内存中,从而提高程序性能。
示例:shm_read.c shm_write.c

最后编辑:
作者:rs232
这个作者貌似有点懒,什么都没有留下。