一、前言
1、在从事嵌入式软件的相关工作中,也许会碰到许多编程技巧,比如,进程间的通信就算一种。大家可以用书本上的知识去指导下实践,反过来也可以用实践去检验书本上知识的正确性(6人还可以更深入地修改、完善参考书上的知识)。当然,笔者只是想整理和巩固下相关知识,并以项目的实际应用,让大家好对进程间的通信知识点重要性的定位,都是些基础知识,大神可以直接略过。在某网络编程之IPC参考书上,源码把许多宏和函数的定义都放在某一个文件中,当运行程序时需要依赖某些库文件;运笔者利用开源的思想,把每个案例所用到宏和函数的定义都提取出来,让大家最终都能看到标准的系统调用,单独编译一个主程序即可运行调试。
2、 首先,介绍下我碰到的实际应用案列。我开发嵌入式设备的需求:看上去有点类似手机的拍照功能,但不同的是运行的是Linux操作系统。首先,ARM的显存(Frame Buffer 0和Frame Buffer 1)有两个部分:一个是用来显示菜单部分的(Qt界面),另一个用来显示CCD图像,然后通过ARM 的叠加IP利用Overlay技术(Arm的 硬件模块,根据手册直接调用驱动接口即可实现),将两层进行叠加显示,大家就可以看到菜单界面的照相机了。
3、 那么Qt在Linux是一个进程,我的理解即单独运行的一个main函数,然后另一个进程就是CCD的预览程序(用到了V4L2显示架构的标准应用调用、DMA传输、线程同步、管道通信等技术)。如果想实现一个拍照工程,那么就在Qt菜单上进行拍照操作时,然后告知CCD预览进程并把要拍照的路径名字传递过去,在这里就用到了FIFO(有名管道),CCD预览程序就会抓一帧底层的数据,再通过ARM 的JPEG模块把拿到的一帧数据编码成图片。
4、 另外,加入在Qt界面还有其他的大量数据(从其他模块得到),比如GPS、日期、天气信息。然后我在拍照时,需要把这些数据添加到CCD照片中(以前听说只要你上传你的照片,就能查出你这张照片在何时何地拍出来的,这也没什么好惊讶的。照片包括了RGB图像数据和一些地理、日期的信息字节流数据,不明白的可以去看https://blog.csdn.net/psy6653/article/details/79658144)。这时又要用到进程间的通信,那么就不是用FIFO了,而是用的共享内存。
二、管道的特性以及开发环境
1、本节主要让大家了解下管道(有名和无名管道),个别函数使用的介绍(fork、watipid ),以及无名管道的演示。后续章节笔者会针对FIFO、锁和共享内存部分进行分析,我会用结合项目的代码进行讲解,并给大家共享源码。
无名管道和有名管道(FIFO)在一般的Linux系统中都是半双工通道,从一边写,从另一边读取,结构图如下,
无名管道一般用在有亲缘关系的父进程和子进程间(一个main函数中调用fork()创建子进程)的通信,而FIFO一般用于无亲缘关系的进程间(Linux操作系统下运行的两个main函数程序)的通信,FIFO往往是是用得最多的。
以下是有亲缘关系的父、子进程利用管道进行通信的结构图,用两根管道把它们连接起来,某著名参考书上管道2的数据流好像画反了。。。
管道1:父进程写数据(fd1[1]),子进程读数据(fd1[0]):
管道2:父进程读数据(fd2[0]),子进程写数据(fd2[1]):
所以fd1[1]和fd2[0]运行在父进程,fd1[0]和fd2[1]运行在子进程
平台:X86 PC
系统:LInux 内核3.1.0(Fedora16)
编译器:X86 gcc
绘图软件:Microsoft Office Visio
截图软件:FastStone_Capture
三、源码的分析
以下是管道通信的源代码
如果想查找某函数的头文件以及函数功能、参数的详细说明(标准系统调用),直接在终端用
`man 函数名`
客户端的源码程序运行在父进程,从管道1的fd1[1]端写数据,从管道2的fd2[0]端读数据,
void client(int readfd, int writefd)
{
size_t len;
ssize_t n;
char buff[MAXLINE];
#ifdef RUN_STEP_DEBUG
printf("[4]从控制台(stdin)读入输入路径\n");
#endif
//等待终端输入文件的绝对路径
fgets(buff, MAXLINE, stdin);/*stdin在<stdio.h>头文件声明*/
len = strlen(buff);/* fgets()保证以字符串空(即null,ASCII值为0)结束*/
if (buff[len-1] == '\n')
len--;/* 删除输入路径中的换行符(即‘\n’,ASCII值为10)*/
#ifdef RUN_STEP_DEBUG
printf("[7]把路径写入进程间管道(fd1[1])\n");
#endif
write(writefd, buff, len);
#ifdef RUN_STEP_DEBUG
printf("[8]从进程间管道(fd2[0])读出内容,并输出到标准输出控制台\n");
#endif
while ( (n = read(readfd, buff, MAXLINE)) > 0)
write(STDOUT_FILENO, buff, n);/*STDOUT_FILENO在<unistd.h>头文件声明*/
}
客户端需要用的strlen和open函数,man strlen命令即可查到,查找其他的函数类似,
服务端的源码程序运行在子进程,从管道1的fd1[0]端读数据,从管道2的fd2[1]端写数据,源码如下,
void server(int readfd, int writefd){
int fd;
ssize_t n;
char buff[MAXLINE+1];
#ifdef RUN_STEP_DEBUG
printf("[6]从进程间管道(fd1[0])读入输入路径\n");
#endif
if ( (n = read(readfd, buff, MAXLINE)) == 0)
buff[n] = '\0';/*路径以null结束 */
if ( (fd = open(buff, O_RDONLY)) < 0) {
#ifdef RUN_STEP_DEBUG
printf("[9]如果打开输入路径失败,向管道(fd2[1])写入提示原因告知客户端\n");
#endif
snprintf(buff + n, sizeof(buff) - n, ": can't open, %s\n",strerror(errno));/*errno在<errno.h>头文件声明*/
n = strlen(buff);
write(writefd, buff, n);
} else {
#ifdef RUN_STEP_DEBUG
printf("[9]如果打开输入路径成功,向管道(fd2[1])写入整个文件内容\n");
#endif
while ( (n = read(fd, buff, MAXLINE)) > 0)
write(writefd, buff, n);
close(fd);
}
}
子进程服务端需要用的snprintf()函数需要的头文件如下,其作用是把数据按照一定的格式放在buff中
main函数的源码如下:
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#define MAXLINE (4096)
#define RUN_STEP_DEBUG
void client(int, int), server(int, int); /*客户端(主进程)和服务端(子进程)函数声明*/
int main(int argc, char **argv){
int fd1[2], fd2[2];
pid_t pid;/*pid_t类型在<sys/types.h>头文件声明*/
#ifdef RUN_STEP_DEBUG
printf("[1]创建2个管道\n");
#endif
pipe(fd1);
pipe(fd2);
#ifdef RUN_STEP_DEBUG
printf("[2]创建一个子进程\n");
#endif
pid = fork();
if (pid < 0){
printf("error in fork!");
}else if ( pid == 0) {
close(fd1[1]);
close(fd2[0]);
#ifdef RUN_STEP_DEBUG
printf("[5]子进程执行服务端函数\n");
#endif
server(fd1[0], fd2[1]);
exit(0);
}else{
close(fd1[0]);
close(fd2[1]);
#ifdef RUN_STEP_DEBUG
printf("[3]执行客户端函数\n");
#endif
client(fd2[0], fd1[1]);
waitpid(pid, NULL, 0);/* 等待子进程结束 */
exit(0);
}
}
以下是pipe、fork、waitpid函数需要的头文件信息。
其中fork函数的使用方法,
1)在父进程中,fork返回新创建子进程的进程ID;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值;
waitpid函数在次是阻塞当前进程,直到子进程结束,接收子进程的结束状态值。根据某参考书上,在此解释下waitpid函数的作用,在子进程调用exit(0)终止后(fork的子进程),但父进程仍然在运行,内核会马上给父进程产生一个SIGCHLD信号,而父进程并没有捕捉该信号(默认是被忽略的),此时已终止的子进程称为僵尸进程。当父进程的client函数从管道读入最终数据返回,再调用waitpid函数获取该僵尸进程的状态。如果父进程没有调用该函数,那么子进程将托孤给init进程(操作系统的守护进程,一直都在运行)的孤儿进程,内核将向init进程发送另外一个SIGCHLD信号,让守护进程取得该终止子进程的终止状态。
makefile
OBJS=mainpipe.o
%.o:%.c
gcc -c $< -o $@
all:$(OBJS)
gcc $(OBJS) -o mainpipe
clean:
rm *.o mainpipe
四、程序的编译测试
运行失败的结果
运行权限的问题
修改文件夹wwww无读、写、执行权限;
下图是程序正常运行的结果:
免费分享演示代码链接: https://pan.baidu.com/s/1JmuZGE0MFxC_aCFEHbuWHA 提取码: dsje