由于在Linux环境下,进程地址空间都是相互独立,每个进程都会有不同的体制空间。任何一个进程的全局变量在另一个进程中都是看不到的,所以进程和进程之间不能相互访问,要交换数据的话必须要通过内核,Linux会在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC InterProcess Communication)。
我们在进程间完成数据传递需要借助操作系统提供特殊的方法,如:文件、管道、信号、共享内存、消息队列、套接字、命名管道等。
随着计算机的蓬勃发展,一些方法由于自身设计缺陷被淘汰或者被弃用。现今常用的进程问通信方式有:
(1)管道(使用最简单)。
(2)信号(开销最小)
(3)共享映射区(无血缘关系)。
(4)本地套接字(最稳定)。
管道是Linux进程之间的通信方式,但是只能作用于有血缘关系的进程之间,为他们完成数据传递的功能,它可以把一个程序的输出连接到另一个程序的输出
调用pipe函数即可创建一个管道,有如下特征:
(1)他只能作用于父子进程或者兄弟进程之间
(2)它是一个半双工模式通信,具有固定的写端和读端
(3)管道也可以看作是一种特殊的文件,对于它的读写使用read和write等函数,但是它不是普通的文件,并不属于其他文件系统且只存在于内存中,内核缓冲区实现
管道的局限性:
(1) 进程不能自己写数据、读数据
(2)管道中数据不可以反复读,一旦读走了,就不再存在
(3)采用半双工通信方式,数据只能在一个方向上流动
(4)只能在有公共祖先的进程间使用管道
常见的通信方式有,单工通信、半双工通信、全双工通信
管道是基于文件描述符的通信方式,当一个管道建立的时候,它会创建两个文件描述符fd[0]和fd[1] ,其中fd[0]固定用于读管道,而fd[0]固定用于写管道,这样就构成了一个半双工的通道
管道关闭时只需要将这两个文件描述符关闭即可,可以使用close函数逐个关闭各个文件描述符
创建管道
int pipe(int fd[2]);
//成功返回 0; f[0]为读端,f[1]为写端
//失败返回 -1,设置error
我们调用了这个函数,如果成功,就会返回r/w两个文件描述符,不用open,但是需要我们手动close
向管道文件读写数据其实就是在读写内核缓冲区
管道创建成功后,创建该管道的进程(父进程)同时掌握着管道的读端和写端,那么我们如何实现父子进程间通信呢?
1. 父进程调用pipe函数创建管道,得到两个文件描述符f[0],f[1]指向管道的读端和写端
2. 父进程调用fork创建子进程,那么子进程也会有两个文件描述符指向同一个管道
3. 父进程关闭管道读端,子进程关闭管道写端,父进程可以往管道中写入数据,子进程将管道中的数据读出来
由于管道是利用环形队列实现的,数据从写端流入管道,从读端流出,这样就形成了进程间的通信
父子通信
例:父子进程使用管道通信,父写入字符串,子进程读出并打印到屏幕上
#include
#include
#include
#include
void sys_err(const char *str)
{
perror(str);
exit(1);
}
int main(int argc, char *argv[])
{
int ret;
int fd[2];
pid_t pid;
char *str = "hello pipe\n";
char buf[1024] = "";
ret = pipe(fd);
if(ret == -1)
{
sys_err("pipe error");
}
pid = fork();
if(pid > 0)
{
close(fd[0]);
write(fd[1], str, strlen(str));
close(fd[1]);
}
else if(pid == 0)
{
close(fd[1]);
ret = read(fd[0], buf, sizeof(buf));
write(STDOUT_FILENO, buf, ret);
close(fd[0]);
}
else
{
sys_err("fork error");
}
return 0;
}
练习:创建一个进程两个管道,从一管道写入小写字符串,另一管道读出大写字符串。
#include
#include
#include
#include
#include
#include
#include
int main()
{
int fd[2]; //管道1
int fd1[2]; //管道2
pid_t pid;
char r_buf[100] = ""; //读取
char w_buf[100] = ""; //写入
char buf[100];
char *p_wbuf = w_buf;
int r_num;
if(pipe(fd) < 0)
{
perror("pipe fd Error");
if(pipe(fd1) < 0)
{
perror("pipe fd1 Error");
exit(-1);
}
pid = fork();
if(pid == 0)
{
close(fd[1]);
close(fd1[0]);
sleep(3);
if((r_num = read(fd[0], r_buf, 100)) > 0)
{
}
for(i = 0; i < r_num; i++)
{
r_buf[i] -= 32;
}
if(write(fd1[1], r_buf, r_num) == -1)
{
printf("写入成功!\n");
}
close(fd1[1]);
close(fd[0]);
exit(0);
}
else if(pid > 0)
{
close(fd[0]);
close(fd1[1]);
sleep(1);
printf("请输入字符串:\n");
scanf("%s", w_buf);
write(fd[1], w_buf, strlen(w_buf));
sleep(1);
if(r_num = read(fd1[0], w_buf, 100) > 0)
{
printf("读取管道内的内容是:%s\n", w_buf);
}
close(fd[1]);
close(fd1[0]);
waitpid(pid, NULL, 0);
}
return 0;
}
兄弟进程通信
#include
#include
#include
#include
#include
#define BUFSIZE 1024
void err_quit(char *str)
{
perror(str);
exit(1);
}
int main(void)
{
int fd[2];
char buf[BUFSIZE];
pid_t pid;
int len;
if((pipe(fd)<0))//创建管道
{
err_quit("pipe");
}
if((pid=fork())<0)//创建第一个子进程
{
err_quit("pipe");
}
else if(pid==0)
{
close(fd[0]);//关闭读端
write(fd[1],"hello brother!",14);//子进程1写入信息
exit(0);
}
else
{
if((pid=fork())<0)//创建第二个子进程
{
err_quit("pipe");
}
else if(pid==0)
{
close(fd[1]);//关闭写端
len=read(fd[0],buf,BUFSIZE);//子进程2读取管道内容
write(STDOUT_FILENO,buf,len);//子进程2写到显示端
printf("\n");
exit(0);
}
else
{//父进程关闭无用端口,
close(fd[0]);
close(fd[1]);
sleep(1);
exit(0);
}
}
}
(1)读管道
1. 管道中有数据:
a. read返回实际读到的字节数,一旦数据读走了,就没有数据了。
b. 可允许多个读端,但存在竞争关系,速度快的进程读走后,数据就莫得了
2. 管道中无数据
a. 管道写端全部关闭,read返回0
b. 写端没有全部被关闭,read阻塞等待
(2)写管道
1. 管道读端全部被关闭,进程异常终止
2. 管道读端没有全部被关闭
a. 管道已满,write阻塞
b. 管道未满,write将数据写入,并返回实际写入的字节数
c. 可以允许多个写端,但存在竞争关系,读会阻塞第一个写的进程,一旦读到内容,就读取结束,如果多个写在读之前全部写完,则全部拿出
测试: 写端打开,3s内部写数据,读端阻塞等待数据,3s后接收到数据停止
#include
#include
#include
#include
int main(int argc, char *argv[] )
{
int ret;
int fd[2];
pid_t pid;
char *str="hello pipe\n";
char buf[1024]="\0";
ret = pipe(fd);
if (ret == -1)
{
perror("pipeError");
}
pid = fork();
if(pid>0)
{
close(fd[0]);
sleep(3);
write(fd[1],str, strlen(str));
close(fd[1]);
}
else if(pid== 0)
{
close(fd[1]);
ret=read(fd[0],buf, sizeof(buf));
write(STDOUT_FILENO,buf,ret);
close(fd[0]);
}
else
{
perror("forkError");
}
return 0;
}
这个函数和Linux的文件操作符中有基于文件流的标准I/O操作一样,管道的操作也指出基于文件流的模式。这种基于文件流的管道,主要是用来创建一个连接到另一个进程的管道,这里的"另一个进程"也就是一个可以进 行一定操作的可执行文件,例如,用户执行”ls -l”或者自己编写的程序”./pipe” 等
由于这类操作很常用,因此标准流管道就将一系列的创建过程合并到一个函数 popen()中完成,它所完 成的工作有以下几步:
(1)创建一个管道
(2)fork()创建一个子进程
(3)在父子进程中关闭不需要的文件描述符
(4)执行exec函数族调用
(5)执行函数中所指定的命令
这个函数的使用可以大大减少代码的编写量,但同时也有一些不利之处。例如,它不如前面管道创建的 函数那样灵活多变,并且用popen()创建的管道必须使用标准I/O函数进行操作,而不能使用系统调用的 read()、write()一类不带缓冲的I/O函数。与之相对应,关闭用popen()创建的流管道必须使用函数 pclose(),该函数关闭标准I/O流,并等待命令执行结束。
FILE * popen( const char * command,const char * type);
如果 type 为 r,那么调用进程读进 command 的标准输出stdout。
如果 type 为 w,那么调用进程写到 command 的标准输入stdin。
若成功则返回文件指针,否则返回NULL,错误原因存于errno中。
popen:创建一个管道
pclose:关闭由popen()打开的管道
例:调用shell命令ls来打印当前目录信息到显示器程序
#include
#include
#include
#include
#define BUFSIZE 1024
int main()
{
FILE *fp;
char *cmd= "ls -l";
char buf[BUFSIZE];
if(NULL == (fp = popen(cmd, "r")))
{
printf("Popen errorl\n");
exit(1);
}
while(NULL != (fgets(buf, BUFSIZE, fp)))
{
printf("%s",buf);
}
printf("\n");
pclose(fp);
exit(0);
}
例:调用shell命令来cat打印buf里面的内容
#include
#include
#include
#include
#define Buffer 100
int main()
{
FILE *write_fp;
char buffer[Buffer + 1];
//把格式化的数据写入某个字符串缓冲区。
//返回值:字符串长度( strlen)
sprintf(buffer,"once upon a time, there was...\n");
/*******格式化输出文件中的数据********///
//-c:等价于-t c, 选择ASCII码字符或者是转义字符
write_fp=popen("cat > 1.txt","w");
if(write_fp != NULL)
{
fwrite( buffer, sizeof(char) , strlen(buffer),write_fp);
pclose(write_fp);
}
}
FIFO是Linux基础文件类型的一种,但是,FIFO文件在磁盘上是没有数据块的,仅仅是用来标识内核中的一条通道。各进程可以打开这个文件进行read/write,实际上实在读写内核通道,那么这样就算是实现了进程之间的通信
有名管道(FIFO)是对无名管道的一种改进,它有以下特点:
(1)它可以使互不相关的两个进程间实现彼此通信
(2)该管道可以通过路径名来指出,并且在文件系统中是可见的。在建立了管道之后,两个进程就可以把它当做普通文件一样进行读写操作,使用非常方便
(3)FIFO严格地遵循先进先出规则,对管道及FIFO的读总是从开始处返回数据,对它们的写则是把数 据添加到末尾,它们不支持如 lseek()等文件定位操作
int mkfifo(const char *pathname,mode_t mode);
//成功: 0;失败: -1
有名管道创建可以使用函数mkfifo(),该函数类似于文件中的open操作,可以指定管道的路径和打开的模式
例:利用mkfifo函数创建FIFO管道
#include
#include
#include
#include
#include
#define BUFSIZE 1024
int main(void)
{
int ret = mkfifo("mytestfifo", 0664);
if(ret==-1)
{
err_quit("mkfifo error");
}
return 0;
}
我们也可以在命令函使用“mkfifo 管道名”来创建有名管道
我们在创建管道成功后,就能使用open、write、read这些函数了。对于为读二打开的管道可在open中设置O_RDONLY,对于为写二打开的管道可以在open中设置O_WRONLY
默认方式:
1. 如果FIFO读没有打开,无法写入内容,write可能处于阻塞状态,当读打开之后,就会立即写入内 容,或者成功write一次后,自动退出
2. 如果FIFO写没有打开,无法读入内容,read处于阻塞状态,当写如内容后,就会立即读到
3. 当unlink()取消有名管道后,write会自动停止,read可能也会停止
例:wrfifo.c 负责写入内容,rdfifo.c负责读出内容
#include
#include
#include
#include
#include
#include
#define BUFSIZE 1024
void err_quit(char *str)
{
perror(str);
exit(1);
}
int main(int argc, char *argv[])
{
int fd, i;
char buf[4096];
if(argc<2)
{
printf("Enter like this: ./a.out fifoname\n");
return -1;
}
fd = open(argv[1],O_WRONLY);
if(fd<0)
{
err_quit("open");
}
i=0;
for(;i<5;i++)
{
sprintf(buf, "hello haifeng %d\n",i+1);
int ret=write(fd, buf, strlen(buf));
printf("write:%s ret=%d\n",buf,ret);
sleep(1);
}
close(fd);
unlink("testfifo");
return 0;
}
#include
#include
#include
#include
#include
#define BUFSIZE 1024
void err_quit(char *str)
{
perror(str);
exit(1);
}
int main(int argc, char *argv[])
{
int fd, len;
char buf[4096];
if(argc<2)
{
printf("Enter like this: ./a.out fifonane\n");
return -1;
}
fd = open(argv[1], O_RDONLY);
if(fd<0)
{
err_quit("open");
}
int i=0;
for(;i<10;i++)
{
len=read(fd,buf,sizeof(buf));
write(STDOUT_FILENO, buf, len);
sleep(1);
}
close(fd);
unlink("testfifo");
return 0;
}
与普通文件不同的是阻塞问题,由于普通文件在读写的时候不会出现阻塞问题,但是在管道的读写中却有阻塞的可能,这里的非阻塞标志可以在open()函数中设定O_NONBLOCK,下面分别对阻塞打开和非阻塞打开的读写进行讨论
对于读进程
(1)若该管道是阻塞打开,且当前FIFO内没有数据,则对读进程而言将一直阻塞到有据写入。 (2)若该管道是非阻塞打开而不能写入全部数据,则读操作进行部分写入或者调用失败。
access()函数的功能是确定文件或文件夹的访问权限,即检查某个文件的存取方式,比如说是只读方式、 只写方式等。如果指定的存取方式有效,则函数返回0,否则函数返回-1。
例:access_wfifo.c负责些内容
#include
#include
#include
#include
#include
#include
#include
int main()
{
int fd;
int w_num;
int ret=mkfifo("myfifo",0664);
printf("mkfifo() ret=%d\n",ret);
if(access("myfifo",F_OK))
{
printf("myfifo no exist\n");
return 1;
}
if (access("myfifo",W_OK) < 0)
{
printf("cannot write fifo.....\n");
return 1;
}
fd = open("myfifo", O_WRONLY);
if(fd==-1)
{
printf("open %s for write error\n", "myfifo");
return 1;
}
w_num= write(fd,"write hello access fifo", strlen("write hello access fifo"));
printf("ret=%d--%s--\n", w_num, "write hello access fifo");
// unlink("testfifo"); //任意一个程序删除FIFO都行
return 0;
}
access_rfifo.c负责读内容
#include
#include
#include
#include
#include
#include
#define FILEMODE (S_IRUSR| S_IWUSR)
int main()
{
char r_buf[50];
int fd;
int r_num;
if (access("myfifo",R_OK) < 0)
{
printf("cannot read fifo.....\n");
return 1;
}
fd = open("myfifo", O_RDONLY);
if(fd==-1)
{
printf("open %s for read error\n", "myfifo");
return 1;
}
r_num= read(fd,r_buf, 50);
printf("Get %d bytes**%s**\n", r_num, r_buf);
// unlink("testfifo");//任意一个程序删除FIFO都行
return 0;
}