一、前情回顾
上一篇主要介绍了IPC进程间通信的信号,了解到信号主要由bash指令的 kill 来触发,c语言的信号处理函数 signal() 实际是一个 void (*sighandler_t) 类型的函数指针,而连续的同种信号发送会造成传统信号的丢失,而实时信号却不会,最后上篇还提出了一个问题:异种信号的连续发送会对传统信号和实时信号产生什么影响?
传统信号:
实时信号:
经由上篇代码小改(此处不展示)便可得如上运行结果,交替运行两种信号发送程序两次,每运行一次程序发送三次该信号并传输相同数据1234,总计12次,最后将每次接收的情况进行打印。
传统信号发送12次共收到2次,信号2和信号3各一种;实时信号发送12次共收到12次,信号40与信号41分隔打印。所以由此现象可得:
不同种传统信号每种只接收一个,其余丢弃;不同种实时信号按同种优先级排队依次处理
上篇链接如下,可相互跳转学习
二、管道概述
上篇的IPC通信 信号 为单向一对多机制,模型为:
而 管道 则为纯粹的单向一对一通信,模型为:
管道是利用内存的一块缓冲区实现数据传输的IPC通信方式,主要分为匿名管道和命名管道,匿名管道只能用于有亲缘关系的进程之间进行通信,而命名管道则可以用于跨任意进程通信
三、匿名管道pipe
3.1 管道符 |
匿名管道最常见的应用便是shell指令的 管道符 |,它通常与刷选搜索指令 grep 同时出现。将每个指令视为一个进程,前者指令的输入便可通过 管道符 | 输出到后者指令的输入
指令1 运行 cat 指令查看打印文件内容;然后指令2 单独运行 grep 0 时由于指令没有输入而产生了阻断,CTRL + C 返回;指令3 利用 管道符 | 让 cat指令的输出作为 grep指令的输入刷选出指定内容,效果等同于指令4 直接给予输入文件。
那既然有同等指令可以实现同样的效果,而且更短,那管道符 | 作用体现在哪里呢?
//tr 命令用于转换或删除文件中的字符, -s 选项为合并同类字符
//cut 命令用于剪切文件中的字节、字符和字段,-d 为自定义分隔符, -f 指定区域
who |grep /2 |tr -s ' ' |cut -d ' ' -f3,4
当我想提取特定会话的创建时间信息时,如果一个个输入还需要加上指令输入的文件名,当指令一多起来未免过于重复和繁琐,而管道符 | 允许创建一个临时通道将所有指令的输入输出连接起来,提升效率
3.2 pipe() 函数
/*****************************************************************************
函数名称 :
#include <unistd.h>
int pipe(int fd[2]);
功能描述 :
创建一个管道来进行两个亲缘进程的数据传输
输入参数 :
fd:一个包含两个整数的数组,用于存储管道的两个端点的文件描述符
fd[0] 用于读取数据,fd[1] 用于写入数据
返 回 值 : 成功返回 0,失败返回 -1
*****************************************************************************/
由于非亲缘进程拥有不同的进程ID和独立的地址空间,而且非亲缘进程间文件描述符符fd无法共享和传递,所以这里还需要另一个函数fork的协助,fork产生的子进程会继承父进程对应的文件描述符
/*****************************************************************************
函数名称 :
#include <unistd.h>
pid_t fork(void);
功能描述 :
创建一个与当前进程几乎完全相同的子进程,将当前进程的内存内容完整的复制到内存的另一个区域
(fork()之后的代码为共同代码,父子进程均会执行)
输入参数 :
无
返 回 值 : 失败返回 -1; 成功子进程返回 0,父进程返回子进程PID
*****************************************************************************/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <string.h>
//信息发送函数
void writefunc(int fd)
{
char *mes = "hellow world";
pid_t self = getpid();
char buff[1024];
memset(buff, 0, sizeof(buff));
snprintf(buff, sizeof(buff), "%s, I am %d", mes, self);
if(write(fd, buff, sizeof(buff)) < 0)
{
perror("write() error\n");
exit(EXIT_FAILURE);
}
}
//信息接受函数
void readfunc(int fd)
{
char buff[1024];
memset(buff, 0, sizeof(buff));
pid_t self = getpid();
if (read(fd, buff, sizeof(buff)) < 0)
{
perror("read() error\n");
exit(EXIT_FAILURE);
}
printf("PID:%d; recv[%s] \n",self, buff);
}
int main() {
int pipefd[2];
if (pipe(pipefd) == -1)
{
perror("pipe() error\n");
exit(EXIT_FAILURE);
}
printf("Before fork Process id: %d\n", getpid());
pid_t pid = fork();
if (pid == -1)
{
perror("fork() error\n");
exit(1);
}
if (pid == 0) //子进程
{
printf("After fork Child Process id: %d\n", getpid());
readfunc(pipefd[0]);
sleep(3);
writefunc(pipefd[1]);
}
else{ //父进程
printf("After fork Parent Process id: %d\n", getpid());
writefunc(pipefd[1]);
sleep(3);
readfunc(pipefd[0]);
wait(NULL); //等待子进程结束回收,防止僵尸进程产生
}
return 0;
}
父进程先给子进程发一个消息,子进程接收到之后打印消息,之后再给父进程发消息,父进程再打印从子进程接收到的消息。程序执行效果:
其中在子进程中的sleep()函数是不必要的,因为在管道中没有数据时,read()读取操作默认是阻塞的,而父进程的sleep()是为了防止父进程运行过快(内存调度失衡)导致父进程自写自读的情况发生,而这也是管道不推荐这种半双工写法的隐患之一,这种写法逻辑图为
通常写法为通过在相应进程内 close(fd[0])或close(fd[1]) 实现一个进程只负责写或读的单工通信
3.3 匿名管道的读写规则
3.3.1 未设置O_NONBLOCK(默认)
a.读快,写慢——>管道为空,读端阻塞;直到管道有数据才继续读取
代码部分在原有的基础上小改一下sleep的间隔就可以了,呈现的效果如下(此为gif):
b.写快,读慢——>管道满载,写端堵塞;直到管道有空位才继续写入
代码在write后加个printf再改一下sleep即可,效果如下:
c.写端关闭,读端一直读——>读端读完管道所有数据,然后read返回0
//信息接受函数
void readfunc(int fd)
{
char buff[1024];
memset(buff, 0, sizeof(buff));
pid_t self = getpid();
int ret = read(fd, buff, sizeof(buff));
if (ret < 0)
{
perror("read() error\n");
exit(EXIT_FAILURE);
}
else if(ret == 0)
{
printf("link down, read return 0\n");
}
else printf("PID:%d; recv[%s] \n",self, buff);
}
/******************************************************/
else{ //父进程
close(pipefd[0]);
printf("After fork Parent Process id: %d\n", getpid());
int count = 3;
while(count > 0){
writefunc(pipefd[1]);
printf("write success, count = [%d]\n", count--);
//sleep(3);
}
close(pipefd[1]);
wait(NULL); //等待子进程结束
}
代码改写也就原基础上信息接收函数的read多加几个判断,父进程加个循环结束条件,效果如下:
d. 读端关闭,写端一直写——>两端进程中断,写端进程返回信号SIGPIPE
if (pid == 0) //子进程
{
close(pipefd[0]);
printf("After fork Parent Process id: %d\n", getpid());
int count = 0;
while(1){
writefunc(pipefd[1]);
printf("write success, count = [%d]\n", count++);
sleep(1);
}
}
else{ //父进程
close(pipefd[1]);
printf("After fork Child Process id: %d\n", getpid());
int count = 3;
while(count > 0){
readfunc(pipefd[0]);
count--;
sleep(2);
}
close(pipefd[0]);
int status = 0;
pid_t ret = waitpid(pid, &status, 0);//等待子进程结束并储存信号编号
if(ret < 0){
perror("waitpid() error\n");
exit(1);
}
//0x7f 是一个掩码,用于提取状态的低 7 位
printf("receve signal:%d\n", status & 0x7f);
}
代码需要让子进程一直写,父进程进行读取,关闭,回收,不然父进程一直写入的话无法进行子进程的状态回收;waitpid()类似于wait()的pro版本,可以储存进程的退出状态或信号编号并指定 waitpid
调用的行为选项,效果如下:
3.3.2 设置O_NONBLOCK(非阻塞)
/*****************************************************************************
函数名称 :
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
功能描述 :
获取或设置文件描述符的属性
输入参数 :
fd:文件描述符,是一个非负整数,指向需要操作的文件或文件描述符。
cmd:指令,指定要执行的操作。
arg:一些 cmd 需要的额外参数,可以是指针或值。
F_DUPFD:
复制 fd 到一个新的文件描述符,并返回新文件描述符的最小值。
参数:期望的新文件描述符的最小值
F_GETFD:
返回文件描述符的标志。
无参数(下文无参数以不写代替)
F_SETFD:
设置文件描述符的标志。
参数:要设置的标志值。
F_GETFL:
返回文件的状态标志,如 O_NONBLOCK(非阻塞) 或 O_APPEND(追加)。
F_SETFL:
设置文件的状态标志。
参数:要设置的标志值。
F_GETLK:
返回对文件的区域加锁信息
参数:指向 struct flock 结构的指针。
F_SETLK:
设置对文件的区域加锁,如果区域已被锁定且不兼容,则阻塞。
参数:指向 struct flock 结构的指针。
F_SETLKW:
设置对文件的区域加锁,等待模式,如果区域已被锁定且不兼容,则等待。
参数:指向 struct flock 结构的指针。
F_GETOWN:
获取进程或线程接收 SIGIO 和 SIGURG 信号的所有权。
F_SETOWN:
设置进程或线程接收 SIGIO 和 SIGURG 信号的所有权。
参数:要设置的进程或线程 ID。
返 回 值 : 成功返回非负值,失败返回 -1
*****************************************************************************/
a.写快,读慢——>管道满载,写端write立刻返回 -1;读端不受影响
代码的改进即在前面未设置O_NONBLOCK的基础上加上fcntl函数和判断,再将信息发送函数writefunc加上printf write的返回结果即可。
当管道被写满后,再次调用write(),会直接返回 -1,读端不受影响,效果如下:
b.读快,写慢——> 读端不关闭时,管道为空,读端read返回-1,写端不受影响
这次我们读端设置非阻断只把时间调慢一点,不设置exit() 退出,为什么呢?大家可以猜一下ψ(`∇´)ψ。管道为空时,读端read() 返回-1 ,效果如下:
四、断言assert()
在我上上一篇文章介绍了预定义宏,它对我们定位报错的代码文件,行数等位置十分有用S/C模型(下),利用UDP、TCP协议和多线程、多路复用实现server的局域网搜索响应与定向实时通信连接https://blog.youkuaiyun.com/qq_59030165/article/details/142376828?spm=1001.2014.3001.5502这次则介绍底层仍是调用这些预定义宏但是更加方便的断言宏assert()
/*****************************************************************************
函数名称 :
#include <assert.h>
void assert(int expression);
功能描述 :
检查一个表达式的值是否为真,如果为假(即为0),则终止程序的执行,并输出一条错误信息
输入参数 :
expression: 需要检查的表达式
返 回 值 : 无
*****************************************************************************/
随便写个判断演示一下
这种判断同样可以用于那些write和read的文件读写,而且assert()更加方便的一点便是它自带一个全局开关宏NDEBUG,定义这个开关之后编译器就会禁用文件中所有的assert()
语句,反之双斜杠注释一下即可,便于debug版和正式版的切换
五、命名管道FIFO
命名管道与匿名管道底层逻辑是一样的,但是命名管道多了一个全局可见的管道文件,这样便允许非亲缘进程也能以文件读写的方式进行通信了。嗯❓❔❓那我直接用普通文件如 .txt 进行数据信息交换不就好了,为什么要弄命名管道呢?ψ(`∇´)ψ 这是因为命名管道多了同步机制(任意时间下文件数据的一致性)和流控制(正确的时间,顺序,大小,阻塞等控制)
5.1 mkfifo与mknod命令
/// @name mkfifo [OPTION]... NAME...
/// @brief 用于创建命名管道FIFO
/// @args -m, –mode=MODE:设置创建的 FIFO 的权限模式,默认为 umask 值。
/// -Z, –context[=CTX]:设置上下文安全性标签。
/// --help:显示帮助信息并退出。
/// --version:显示版本信息并退出。
演示:
用 mkfifo 命令创建的管道文件标识为p(红框部分),初始权限umask值为644(rwx为111<二进制>==7<十进制>,表示读写执行,三个rwx分别代表用户,用户组,其他),创建的两个会话能实现通信“hello world” ,而 mknod 的 用法也与之类似
/// @name mknod [OPTIONS] NAME TYPE [MAJOR MINOR]
/// @brief 用于创建设备文件节点
/// @args [OPTIONS]:
/// -m:设置文件模式(权限)
/// -Z:设置安全上下文
/// TYPE:
/// b(块设备)或 c(字符设备)或 p(管道FIFO)
/// [MAJOR MINOR]:
/// 主次设备号
此处只演示mknod的管道创建:
5.2 mkfifo() 与 mknod() 函数
/*****************************************************************************
函数名称 :
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
功能描述 :
创建一个命名管道
输入参数 :
pathname: 要创建的命名管道的路径名
mode: 设置管道的权限模式
返 回 值 : 成功返回 0,失败返回 -1
*****************************************************************************/
/*****************************************************************************
函数名称 :
#include <sys/types.h>
#include <sys/stat.h>
int mknod(const char *pathname, mode_t mode, dev_t dev);
功能描述 :
创建一个设备文件
输入参数 :
pathname: 要创建的文件的路径名。
mode: 文件的权限和类型。
S_IFMT:文件类型掩码,用于提取文件类型。
S_IFREG:常规文件。
S_IFDIR:目录。
S_IFCHR:字符设备文件。
S_IFBLK:块设备文件。
S_IFIFO:FIFO 或命名管道。
S_IFLNK:符号链接。
S_IFSOCK:套接字
dev: 设备特定的标识符。(组合主次设备号dev_t dev = makedev(major, minor))
返 回 值 : 成功返回 0,失败返回 -1
*****************************************************************************/
//服务端111.c ,负责管道创建,信息接受和管道清除
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <assert.h>
//#define NDEBUG
//信息接受函数
void readfunc(int fd)
{
char buff[1024];
memset(buff, 0, sizeof(buff));
pid_t self = getpid();
assert(read(fd, buff, sizeof(buff)) >= 0);
printf("PID:%d; recv[%s] \n",self, buff);
}
int main(int argc, char *argv[])
{
assert(argc == 2);//运行时输入要创建的管道名
assert(mkfifo(argv[1], 0600) >= 0);//0600的第一个0代表十进制
//assert(mknod(argv[1], 0600|S_IFIFO, 0) >= 0);//可等效替代
int fifofd = open(argv[1], O_RDONLY);
assert(fifofd >= 0);
for(int i = 1; i <= 3; i++)
readfunc(fifofd);
close(fifofd);
int m = unlink(argv[1]); //删除命名管道
assert(m == 0);
return 0;
}
//客户端112.c, 负责管道信息发送
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <assert.h>
//#define NDEBUG
//信息发送函数
void writefunc(int fd, char *mess)
{
char *mes = mess;
pid_t self = getpid();
char buff[1024];
memset(buff, 0, sizeof(buff));
snprintf(buff, sizeof(buff), "%s, I am %d", mes, self);
assert(write(fd, buff, sizeof(buff)) >= 0);
}
int main(int argc, char *argv[])
{
assert(argc == 3);//运行时输入创建的管道名和发送的信息
int fifofd = open(argv[1], O_WRONLY);
assert(fifofd >= 0);
for(int i = 1; i <= 3; i++)
{
writefunc(fifofd, argv[2]);
sleep(5);//此时可以ps -al查看进程PID
}
close(fifofd);
return 0;
}
运行展示:
这里我打开了三个会话窗口,一个运行管道服务端111.c创建管道接受信息,一个运行管道客户端112.c发送信息,一个运行ps查看两个程序的进程id;利用上上篇讲的main函数参数实现自定义管道名为123,自定义传送信息为helloworld, 同时也用刚介绍的断言assert() 对一些判断做了优化替换。
六、总结
本篇首先对上一篇异种信号的连续发送作了补充;接着进入正题,主要对匿名管道pipe、命名管道FIFO的Linux指令、对应函数以及不同情况的读写规则进行了描述,中间还穿插了断言机制用于预定义宏的替换与debug的优化;最后回答 3.3.2 提出的问题为什么非阻断读快写慢时不设置读取错误时退出读端?因为设置exit() 后就与 未设置O_NONBLOCK的情况是一样的,读端(子)退出后,写端进程(父)返回SIGPIPE,两进程均终止,即便此时换成父读子写也是如此;怎样,与你所想一致嘛\( ̄︶ ̄*\))