😋😋😋😋😋😋😋😋😋😋😋😋😋 Linux专栏开源,点点我,一起走进开源世界 😋😋😋😋😋😋😋😋😋😋😋😋😋
第一话-》匿名管道pipe
【1】进程间通信(IPC)概览
在Linux/Unix 系统中,进程间通信方式(Inter-Process Comunication)通常有如下若干中方式:
管道
| |
---|
匿名管道 pipe : | 适用于亲缘关系进程间的、一对一的通信 | 具名管道 fifo : | 适用于任何进程间的一对一、多对一的通信 | 套接字 socket : | 适用于跨网络的进程间通信 | 信号 : | 异步通信方式 |
system-V IPC对象
| |
---|
共享内存 : | 效率最高的通信方式 | 消息队列 : | 相当于带标签的增强版管道 | 信号量组 : | 也称为信号灯,用来协调进程间或线程间的执行进度 |
POSIX信号量
| |
---|
POSIX匿名信号量 : | 适用于多线程,参数简单,接口明晰,童叟无欺 | POSIX具名信号量 : | 适用于多进程,参数简单,接口明晰,老少咸宜 |
这些通信机制统称IPC,它们各有特色,各有适用的场合。
【2】 匿名管道PIPE
2.1 基本逻辑
不管是匿名管道还是具名管道,在Linux系统下都属于文件的范畴,区别是匿名管道没有名称 ,因此无法使用open创建或打开 ,事实上匿名管道有自己独特的创建接口,但其读写方式与普通的文件一样,支持read()/write()操作 。
管道文件事实上还包括网络编程中的核心概念套接字 ,所谓的管道指的是这些文件不能进行“定位” ,只能顺序对其读写数据,就像一根水管,拧开水龙头不断读取,就可以源源不断读到水管中的数据,但如果没有水出来那只能继续等待,不能试图“跳过”部分文件去读写水管的中间地带,这是管道的最基本的特性。
2.2 函数接口 创建匿名管道的函数接口非常简单,如下所示:
#include <unistd.h>
int pipe( int fd[2] );
注意1: 由于匿名管道拥有两个文件描述符 ,一个专用于读fd[0],一个专用于写fd[1] ,因此上述接口需要传递一个至少包含两个整型元素的数组过去,用来存放这两个特定的描述符。
注意2: 匿名管道描述符,只能通过继承的方式传递给后代进程,因此只能用于亲缘进程间的通信,由于没有文件名,其他非亲缘进程无法获取匿名管道的描述符。
注意3: 不能有多个进程同时对匿名管道进行写操作,否则数据有可能被覆盖。
总结一句话,匿名管道适用于一对一的、具有亲缘关系的进程间的通信。
下面以父子进程使用匿名管道通信的例子对PIPE的使用加以说明,假设父进程先创建一条匿名管道,然后产生一个子进程,此时子进程自然继承了这条管道的读写端描述符,进而它们就可以通信了。
示例代码如下:
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main(void)
{
int fd[2];
pipe(fd);
if(fork() == 0)
{
char *msg = "hello parent!";
write(fd[1], msg, strlen(msg));
exit(0);
}
else
{
char buf[50];
bzero(buf, 50);
read(fd[0], buf, 50);
printf("来自子进程: %s\n", buf);
exit(0);
}
}
注意,匿名管道的读写端是严格区分的,任何不规范的操作都是不允许的,其结果都是不确定的。
另外还应该注意到,一般而言,不需要用到的文件描述符都最好及时关闭,避免不必要的副作用或浪费系统资源。例如上述程序中,子进程只用到了管道的写端,因此它的fd[0]可以也应该要关闭,相反父进程只用到了管道的读端,因此它的fd[1]可以也应该关闭。
代码可以改成:
int main(void)
{
int fd[2];
pipe(fd);
if(fork() == 0)
{
close(fd[0]);
...
}
else
{
close(fd[1]);
...
}
}
2.3 管道的读写特性
当我们对一个管道文件(包括匿名管道、具名管道和网络socket)进行读写操作时,我们需要知道将会发生什么,比如读一个空管道会怎么样?对一个缓冲区已满的管道执行写入操作会怎么样等等,可以对这些读写操作做一个统一的整理。
术语约定:
读者: 对管道拥有读权限的进程
写者: 对管道拥有写权限的进程
注意,所谓的读者、写者不是只正在读或者正在写的进程,而是只要拥有读写权限就称为管道的读者写者,比如如下进程关闭了匿名管道的读端,因此它只能称为匿名管道的写者:
int fd[2];
pipe(fd);
close(fd[0]);
int fd = open("fifo", O_RDWR);
下面是读写特性对照表:
2.4 管道的阻塞特性
仔细看管道读写特性的表会发现,当试图读取一个空管道 ,或者试图写入一个缓冲区已满的管道时 ,读写操作默认会进入所谓“阻塞(se) ”的状态。所谓的阻塞实际上就是系统将该进程挂起 ,等待资源就绪再继续调度的一种状态,这种阻塞的状态有利于系统中别的进程可高效地使用闲置CPU资源 ,提高系统的吞吐量。
对于阻塞而言,有如下特性需要记忆:
以下是设置管道文件阻塞特性的代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
int main()
{
int fd[2];
pipe(fd);
long flag = fcntl(fd[0], F_GETFL);
flag |= O_NONBLOCK;
fcntl(fd[0], F_SETFL, flag);
int n;
char buf[20];
n = read(fd[0], buf, 20);
if(n < 0)
perror("read failed");
flag = fcntl(fd[0], F_GETFL);
flag &= ~O_NONBLOCK;
fcntl(fd[0], F_SETFL, flag);
n = read(fd[0], buf, 20);
if(n < 0)
perror("read failed");
return 0;
}
注意: 管道打开时,必须同时有读者和写者,否则 open 也会阻塞。
第二话-》具名管道FIFO
【3】具名管道FIFO概述
具名管道是跟匿名管道相对而言的,从外在形态上来看,具名管道更接近普通文件,有文件名、可以open打开、支持read()/write()等读写操作 。
具名管道通常又被称为FIFO(First In First Out) ,这其实所所有管道的基本特性,那就是放入的数据都是按顺序被读出,即所谓先进先出 的逻辑。
当然,管道并不是普通文件,具名管道特性:
与PIPE一样不支持定位操作lseek() 与PIPE一样秉持相同的管道读写特性 使用专门的接口来创建:mkfifo()(匿名管道是pipe()) 在文件系统中有对应节点,支持使用 open() 打开管道(匿名管道不具备) 支持多路同时写入(匿名管道不具备)
【4】 函数接口
以下是创建具名管道的函数接口
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
注意1:pathname即具名管道的名称,若是新建的管道文件,则需保证创建路径位于Linux系统内,尤其是虚拟机中操作的时候,不可将管道文件创建在共享文件夹中,因为共享文件夹是windows系统,不支持管道文件。
注意2:mode是文件权限模式,例如0666,注意权限须为八进制,且实际管道的权限还受系统 umask 的影响。
以下是两个程序通过管道互相通信的示例代码:
int main()
{
mkfifo("/tmp/fifo", 0666);
int fd = open("/tmp/fifo", O_RDWR);
char *msg = "data from FIFO";
write(fd, msg, strlen(msg));
close(fd);
return 0;
}
int main()
{
int fd = open("/tmp/fifo", O_RDWR);
char buf[50];
bzero(buf, 50);
read(fd, buf, 50);
printf("%s\n", buf);
close(fd);
return 0;
}
注意3:具名管道一旦没有任何读者和写者,系统判定管道处于空闲状态,会释放管道中的所有数据。
第三话-》面试题
问:老师,什么是无名管道和有名管道?
答:这些都是翻译的问题,PIPE一般被称为匿名管道,也称为无名管道;FIFO一般被称为具名管道,也称为有名管道、命名管道等。
问:老师,什么时候用PIPE?什么时候用FIFO?
答:亲缘进程间的少量数据交互,并且是一对一的情况下,使用匿名管道即可。如果不是亲缘进程,那就无法使用匿名管道。如果是多对一通信,比如经典的日志类系统,多个进程会同时向同一个管道写入数据,由于匿名管道不保证写入的原子性,也就是同时写入的数据可能会互相践踏破坏,因此这种情况也不能使用匿名管道,而可以使用具名管道。还有就是如果传输的数据量比较大,并且比较在意时效性,那么这两种管道可能都不合适,也许可以考虑本地域套接字或共享内存。
编程实现下述命令的执行效果,查看系统进程列表中的指定进程信息:
gec@ubuntu:~$ ps ajx | grep 'xxx' --color
253
gec@ubuntu:~$
解答:
这道题结合了进程基本API和匿名管道、文件描述符重载等知识,首先需要创建两个进程,并用管道将它们联系起来,再分别去加载ps和grep这两个程序,利用文件描述符重载技术,将ps和grep的输入和输出用管道联通起来。
以下是示例代码:
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main(int argc, char **argv)
{
int fd[2];
pipe(fd);
if(argc != 2)
{
printf("用法: %s 要搜索的进程名\n", argv[0]);
exit(0);
}
if(fork() == 0)
{
dup2(fd[1], STDOUT_FILENO);
execlp("ps", "ps", "ajx", NULL);
}
else
{
close(fd[1]);
dup2(fd[0], STDIN_FILENO);
execlp("grep", "grep", argv[1], "--color", NULL);
}
exit(0);
}
(exec函数族、匿名管道、文件描述符重定向)
【1】编程实现下述命令的执行效果,计算指定目录下的文件个数:
gec@ubuntu:~$ ls /etc/ | wc -w
253
gec@ubuntu:~$
解答: 这道题跟本节随堂练习一样,主要要注意在负责读取数据的进程中,要正确关闭不需要的管道写端,否则会导致自身的循环IO阻塞等待而无法退出。
以下是示例代码:
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main(int argc, char **argv)
{
int fd[2];
pipe(fd);
if(argc != 2)
{
printf("用法: %s 指定文件夹\n", argv[0]);
exit(0);
}
if(fork() == 0)
{
dup2(fd[1], STDOUT_FILENO);
execlp("ls", "ls", argv[1], NULL);
}
else
{
close(fd[1]);
dup2(fd[0], STDIN_FILENO);
execlp("wc", "wc", "-w", NULL);
}
exit(0);
}
(精灵进程、有名管道FIFO)
【2】编程实现两个程序:一个服务器server,一个客户机client。 要求:
服务器创建并监视有名管道FIFO,一旦发现有数据则将其保存到一个指定的地方。 客户机每隔一段时间产生一个子进程。 客户机的这些子进程将当前系统时间和自身的PID写入有名管道FIFO就退出。
解答:
int main(int argc, char **argv)
{
mkfifo("/tmp/fifo", 0666);
int fifofd = open("/tmp/fifo", O_RDWR);
int logfd = open("/tmp/log.txt", O_WRONLY|O_CREAT|O_APPEND, 0777);
char buf[1024];
while(1)
{
bzero(buf, 1024);
read(fifofd, buf, 1024);
write(logfd, buf, strlen(buf));
}
return 0;
}
int main(int argc, char **argv)
{
mkfifo("/tmp/fifo", 0777);
int fd = open("/tmp/fifo", O_WRONLY);
char buf[1024];
time_t t;
while(1)
{
bzero(buf, 1024);
time(&t);
snprintf(buf, 1024, "[%-6d] %s",
getpid(), ctime(&t));
write(fd, buf, strlen(buf));
sleep(1);
}
return 0;
}
(管道读写特性、阻塞与非阻塞)
【3】根据管道的读写特性,编写一个程序,测试你系统管道文件的缓冲区大小。
解答:
根据管道读写特性可知,当管道的缓冲区满了再继续写入数据时,默认会阻塞,这将无法让程序正常汇报缓冲区的尺寸,因此只需在写入测试数据前将管道设置为非阻塞即可。
以下以匿名管道为例,展示示例代码:
int main()
{
int fd[2];
pipe(fd);
long flag = fcntl(fd[1], F_GETFL);
flag |= O_NONBLOCK;
fcntl(fd[1], F_SETFL, flag);
int total = 0;
while(1)
{
if(write(fd[1], "x", 1) < 0)
{
perror("写入失败");
break;
}
total++;
}
printf("缓冲区大小:%d个字节\n", total);
}
#端午趣味征文赛–用代码过端午#
|