嵌入式Linux应用开发
第一章. Framebuffer应用开发
Framebuffer应用开发
第一章. Framebuffer应用开发
1.1 LCD Framebuffer操作原理
LCD Framebuffer 就是一块显存,在嵌入式系统中,显存是被包含在内存中。LCD Framebuffer里的若干字节(根据驱动程序对LCD控制器的配置而定)表示LCD屏幕中的一个像素点,一一对应整个LCD屏幕。举个例子,LCD屏幕是800600的分辨率,即LCD屏幕存在480000个像素点,若每个像素点4个字节表示,那么LCD Framebuffer显存大小为4800004=960000字节,即1.92MB。因此我们的内存将会分割至少1.92MB的空间用作显存。具体地址在哪里,这个就是又驱动程序去定,应用程序只需直接使用即可,硬件相关操作已由驱动程序封装好。
如上图,我们只需要往Framebuffer中填入不同的值,驱动程序和硬件控制器就会把这些数据传输到对应LCD屏幕上的像素点,从而显示不同的颜色。由此可知,我们应用程序只需要针对Framebuffer操作即可,其他交给驱动程序和硬件。
1.2 Framebuffer API接口
1.2.1 open系统调用
头文件:#include <sys/types.h>,#include <sys/stat.h>,#include <fcntl.h>
函数原型:
- int open(const char *pathname, int flags);
- int open(const char *pathname, int flags, mode_t mode);
函数说明:
-
pathname 表示打开文件的路径;
-
Flags表示打开文件的方式,常用的有以下6种,
①:O_RDWR表示可读可写方式打开;
②:O_RDONLY表示只读方式打开;
③:O_WRONLY表示只写方式打开;
④:O_APPEND 表示如果这个文件中本来是有内容的,则新写入的内容会接续到原来内容的后面;
⑤:O_TRUNC表示如果这个文件中本来是有内容的,则原来的内容会被丢弃,截断;
⑥:O_CREAT表示当前打开文件不存在,我们创建它并打开它,通常与O_EXCL结合使用,当没有文件时创建文件,有这个文件时会报错提醒我们;
Mode表示创建文件的权限,只有在flags中使用了O_CREAT时才有效,否则忽略。
返回值:打开成功返回文件描述符,失败将返回-1。
1.2.2 ioctl系统调用
头文件:#include <sys/ioctl.h>
函数原型:
- int ioctl(int fd, unsigned long request, ...);
函数说明:
- fd 表示文件描述符;
- request表示与驱动程序交互的命令,用不同的命令控制驱动程序输出我们需要的数据;
- … 表示可变参数arg,根据request命令,设备驱动程序返回输出的数据。
返回值:打开成功返回文件描述符,失败将返回-1。
1.2.3 mmap系统调用
头文件:#include <sys/mman.h>
函数原型:
- void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
函数说明:
-
addr表示指定映射的內存起始地址,通常设为 NULL表示让系统自动选定地址,并在成功映射后返回该地址;
-
length表示将文件中多大的内容映射到内存中;
-
prot 表示映射区域的保护方式,可以为以下4种方式的组合
①PROT_EXEC 映射区域可被执行
②PROT_READ 映射区域可被读写
③PROT_WRITE 映射区域可被写入
④PROT_NONE 映射区域不能存取
-
Flags 表示影响映射区域的不同特性,常用的有以下两种
①MAP_SHARED 表示对映射区域写入的数据会复制回文件内,原来的文件会改变。
②MAP_PRIVATE 表示对映射区域的操作会产生一个映射文件的复制,对此区域的任何修改都不会写回原来的文件内容中。
返回值:若成功映射,将返回指向映射的区域的指针,失败将返回-1。
1.3 在LCD上描点操作
1.3.1 在LCD上显示点阵理论基础
如上图,当我们需要显示一个字母‘A’时,是通过判断点阵的每一个位数值状态,来填充颜色,达到显示字符效果。其中‘1’表示一种颜色,‘0’表示填充另一种颜色。上图的是8*16的点阵,我们也可以用其他不同大小点阵,只要有这个点阵,我们就可以在LCD上面描点,达到显示字符的效果。
1.3.2 获取fb_var_screeninfo结构体
在用点阵显示字符之前,我们需要先从设备fb0中获取相关的LCD信息,下图截取我们将用到的fb_info结构体部分内容。
通过系统调用ioctl,获取xres(x方向总像素点),yres(y方向总像素点),bits_per_pixel(每个像素点占据的位数),根据获取的三个资源,外加点阵,根据这四个资源,我们就可以显示一个字符。
程序文件:show_ascii.c
4718 fd_fb = open("/dev/fb0", O_RDWR);
4719 if (fd_fb < 0)
4720 {
4721 printf("can't open /dev/fb0\n");
4722 return -1;
4723 }
4724 if (ioctl(fd_fb, FBIOGET_VSCREENINFO, &var))
4725 {
4726 printf("can't get var\n");
4727 return -1;
4728 }
先打开LCD设备(fb0),获得文件描述符,再通过ioctl获取fb_var_screeninfo信息并保存在var变量,后续只需访问var这个结构体,就可以获得xres(x方向总像素点),yres(y方向总像素点),bits_per_pixel(每个像素点占据的位数)这三个关于fb0的资源。
1.3.3 根据fb_var_screeninfo计算变量
fb_var_screeninfo已保存在var结构体变量中,接着来访问var结构体变量即可
根据xres与bits_per_pixel算出每行像素点所占据的字节数
程序文件:show_ascii.c
4730 line_width = var.xres * var.bits_per_pixel / 8;
根据bits_per_pixel算出每个像素点所占据的字节数
程序文件:show_ascii.c
4731 pixel_width = var.bits_per_pixel / 8;
根据xres,yres,bits_per_pixel算出全部像素点所占据的字节总和
程序文件:show_ascii.c
4732 screen_size = var.xres * var.yres * var.bits_per_pixel / 8;
1.3.4 使用mmap系统调用,映射内存
程序文件:show_ascii.c
4733 fbmem = (unsigned char *)mmap(NULL , screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb, 0);
4734 if (fbmem == (unsigned char *)-1)
4735 {
4736 printf("can't mmap\n");
4737 return -1;
4738 }
4739
4740 /* 清屏: 全部设为黑色 */
4741 memset(fbmem, 0, screen_size);
调用mmap将显存映射在内存中,以可读可写(PROT_READ | PROT_WRITE)及内存回写(MAP_SHARED)的方式映射,从而获得一个指向映射在内存空间的首地址fbmem,后续操作就是在这个首地址的基础上计算各种不同的偏移量,填充颜色值。
1.3.5 描点函数编写
程序文件:show_ascii.c
4641 void lcd_put_pixel(int x, int y, unsigned int color)
描点函数有3个参数,x坐标,y坐标,像素点颜色值。
程序文件:show_ascii.c
4643 unsigned char *pen_8 = fbmem+y*line_width+x*pixel_width;
4644 unsigned short *pen_16;
4645 unsigned int *pen_32;
4646
4647 unsigned int red, green, blue;
4648
4649 pen_16 = (unsigned short *)pen_8;
4650 pen_32 = (unsigned int *)pen_8;
在此处函数参数x与y表示的是像素点的坐标,而单个像素点所占据的显存大小可能会有不同的情况出现,如1字节表示一个像素点,2字节表示一个像素点,4字节表示一个像素点等,为了更多的兼容不同的情况,因此申请3个指针,pen_8指向的是占据1个字节的像素点空间, pen_16指向的是占据2个字节的像素点空间,pen_32指向的是占据4个字节的像素点空间。
fbmem是系统调用mmap返回的显存首地址,根据fbmem计算填充颜色的内存空间。
当像素点占据1个字节空间时
对应描点地址= fbmem+Y * 一行所占据的字节数 + x * 每个像素点所占据的字节数
程序文件:show_ascii.c
4652 switch (var.bits_per_pixel)
4653 {
4654 case 8:
4655 {
4656 *pen_8 = color;
4657 break;
4658 }
4659 case 16:
4660 {
4661 /* 565 */
4662 red = (color >> 16) & 0xff;
4663 green = (color >> 8) & 0xff;
4664 blue = (color >> 0) & 0xff;
4665 color = ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3);
4666 *pen_16 = color;
4667 break;
4668 }
4669 case 32:
4670 {
4671 *pen_32 = color;
4672 break;
4673 }
4674 default:
4675 {
4676 printf("can't surport %dbpp\n", var.bits_per_pixel);
4677 break;
4678 }
4679 }
4680 }
根据设备fb0实际的bits_per_pixel值,选择对应的pen(pen_8,pen_16,pen_32其中一个),最后把color颜色变量传入选择的pen中。
1.4 在LCD上使用点阵写字
1.4.1 在LCD上显示英文字母
①找出英文字母在点阵数组中的地址,c所代表的是一个英文字母(ASCII值)。
程序文件:show_ascii.c
4693 unsigned char *dots = (unsigned char *)&fontdata_8x16[c*16];
②根据获得的英文字母点阵,每一位依次判断,描点,‘1’表示白色,‘0’表示黑色。
根据上图,我们分析下如何利用点阵在LCD上显示一个英文字母,因为有十六行,所以首先要有一个循环16次的大循环,然后每一行里有8位,那么在每一个大循环里也需要一个循环8次的小循环,小循环里的判断单行的描点情况,如果是1,就填充白色,如果是0就填充黑色,如此一来,就可以显示出黑色底,白色轮廓的英文字母。
程序文件:show_ascii.c
4697 for (i = 0; i < 16; i++)
4698 {
4699 byte = dots[i];
4700 for (b = 7; b >= 0; b--)
4701 {
4702 if (byte & (1<<b))
4703 {
4704 /* show */
4705 lcd_put_pixel(x+7-b, y+i, 0xffffff); /* 白 */
4706 }
4707 else
4708 {
4709 /* hide */
4710 lcd_put_pixel(x+7-b, y+i, 0); /* 黑 */
4711 }
4712 }
4713 }
③调用我们编写的lcd_put_ascii函数
程序文件:show_ascii.c
4743 lcd_put_ascii(var.xres/2, var.yres/2, 'A'); /*在屏幕中间显示8*16的字母A*/
④编译c文件show_ascii.c
编译命令:arm-linux-gnueabihf-gcc -o show_ascii show_ascii.c
⑤将编译出来的show_ascii传输到开发板,并进入show_ascii的目录下
执行命令:./show_ascii
如果实验成功,我们将看到屏幕中间会显示出一个白色的字母‘A’。
1.4.2 在LCD上显示汉字
与显示英文字母有点不同,因为汉字的点阵我们是需要通过汉字库提取出来,并没有直接提供点阵数组,因此我们程序开头需要打开汉字库文件(HZK16),然后再找到相应的位置,提取出汉字的点阵,最后再按显示英文字母一样显示它,不过这个汉字是16*16的。
① 打开汉字库文件
程序文件:show_font.c
4760 fd_hzk16 = open("HZK16", O_RDONLY);
② 获取汉字库文件的属性,存在hzk_stat结构体变量中
程序文件:show_font.c
4793 if(fstat(fd_hzk16, &hzk_stat))
此处主要是用知道该文件的大小,因为后面mmap时需要知道映射的文件大小。
③使用mmap系统调用
程序文件:show_font.c
4798 hzkmem = (unsigned char *)mmap(NULL , hzk_stat.st_size, PROT_READ, MAP_SHARED, fd_hzk16, 0);
hzkmem与fbmem类似,也是一个指向映射内存的指针,但是它是指向汉字库,方便
后续计算汉字点阵偏移位置用。
④使用汉字库,调出点阵显示汉字
HZK16 字库是符合GB2312标准的16×16点阵字库HZK16的编码,每个字需要32个字节的点阵来表示,例如我们将要显示的‘中’字,编码是D6D0,难道就是2个字节表示吗?不是说32字节吗?D6D0编码是一个类似于索引码,D6是区码,D0是位码,先要找到D6-A1才是真正区,在D6-A1区里找到D0-A1的真正位置,这才是‘中’字点阵的起始位置(减去A1是为了兼容ascii),每一个区有94个汉字。
程序文件:show_font.c
4734 unsigned int area = str[0] - 0xA1;
4735 unsigned int where = str[1] - 0xA1;
4736 unsigned char *dots = hzkmem + (area * 94 + where)*32;
上图是汉字点阵排布的示意图,总共有十六行,因此需要一个循环16次的大循环,考虑到一行有两个字节,我们大循环中加入一个循环2次的循环用于区分是哪个字节,最后判断当前字节的每一位,如果为 ‘1’描白色,如果为‘0’描黑色
程序文件:show_font.c
4740 for (i = 0; i < 16; i++)
4741 for (j = 0; j < 2; j++)
4742 {
4743 byte = dots[i*2 + j];
4744 for (b = 7; b >=0; b--)
4745 {
4746 if (byte & (1<<b))
4747 {
4748 /* show */
4749 lcd_put_pixel(x+j*8+7-b, y+i, 0xffffff); /* 白 */
4750 }
4751 else
4752 {
4753 /* hide */
4754 lcd_put_pixel(x+j*8+7-b, y+i, 0); /* 黑 */
4755 }
4756 }
4757 }
⑤调用我们编写的lcd_put_chinese函数
程序文件:show_font.c
4810 printf("chinese code: %02x %02x\n", str[0], str[1]);
4811 lcd_put_chinese(var.xres/2 + 8, var.yres/2, str);
⑥编译c文件show_font.c
编译命令:arm-linux-gnueabihf-gcc -o show_font show_font.c
注:使用此命令HZK16文件必须与show_font.C在同一目录下。
⑦将编译出来的show_font传输到开发板,并进入show_font的目录下
执行命令:./show_font
如果实验成功,我们将看到屏幕中间会显示出一个白色的字母‘A’与汉字‘中’,同时在串口打印信息中看到‘中’对应的编码。
chinese code: d6 d0
1.5 搭建freetype相关环境
1.5.1 交叉编译freetype,并安装
①解压freetype源文件
tar xjf freetype-2.4.10.tar.bz2
②进入解压后的freetype-2.4.10目录
cd freetype-2.4.10
③配置freetype-2.4.10
./configure --host=arm-linux-gnueabihf --prefix=/home/book/100ask_imx6ull-sdk/ToolChain/gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf/libc/usr/
④建个目录,避免后面安装出错提示缺少这个internal目录
mkdir /home/book/100ask_imx6ull-sdk/ToolChain/gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf/libc/usr/include/freetype2/freetype/internal -p
④编译
make
⑤安装
make install
⑥移动freetype头文件,避免以后编译总是需要指定头文件路径
mv /home/book/100ask_imx6ull-sdk/ToolChain/gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf/libc/usr/include/freetype2/freetype /home/book/100ask_imx6ull-sdk/ToolChain/gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf/libc/usr/include/
1.5.2 freetype库,头文件移植至开发板
由于100ask开发板已经有freetype相关的库和头文件,因此不需要移植,如果开发板没有freetype库和头文件就需要按以下方法移植
/home/book/100ask_imx6ull-sdk/ToolChain/gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf/libc/usr/include/* 复制到开发板的头文件目录中
/home/book/100ask_imx6ull-sdk/ToolChain/gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf/libc/usr/lib/so 复制到开发板的库文件目录中
注:链接文件需要保持它的链接属性(即加-d选项)。
1.6 使用freetype
1.5.1 矢量字体引入
点阵显示英文字母,汉字时,大小固定,如果放大会有锯齿出现,为了解决这个问题,引用矢量字体。
矢量字体形成分三步,若干的关键点,数学曲线(贝塞尔曲线),填充颜色组合而成。
①假设A字母的关键点如图中的黄色圈圈,确定关键点。
②用数学曲线将关键点都连接起来,成为封闭的曲线。
③最后把封闭空间填满颜色,就显示出一个A字母。
如果需要放大或者缩小字体,关键点的相对位置是不变的,跟进放大比例放大或缩小,但是相对位置不变,好像分数中的1/2 和 2/4,比例是不变的,但是值却大了,类似这个味道。
1.5.2 Freetype理论介绍
开源的Freetype字体引擎库它提供统一的接口来访问多种字体格式文件,从而实现矢量字体显示。我们只需要移植这个字体引擎,调用对应的API接口,提供字体关键点,就可以让freetype库帮我们实现闭合曲线,填充颜色,达到显示矢量字体的目的。
关键点(glyph)存在字体文件中,Windows使用的字体文件在FONTS目录下,扩展名为TTF的都是矢量字库,本次使用实验使用的是新宋字体simsun.ttc。
字体文件结构如上图
Charmaps表示字符映射表,字体文件可能支持哪一些编码,GBK,UNICODE,BIG5还是别的编码,如果字体文件支持该编码,跟进编码,通过charmap,找到对应的glyph,一般而言都支持UNICODE码。
有了以上基础,我们想象一个文字的显示过程
- ①给定一个文字吗‘A’(0x41),‘中’(GBK,UNICODE ,BIG5)可以确定它的编码值;
- ②跟进编码值,从枝头文件中通过charmap找到对应的关键点(glyph);
- ③设置字体大;
- ④用某些函数把关键点(glyph)缩放为我们设置的字体大小;
- ⑤转换为位图点阵
- ⑥在LCD上显示出来
如上图,参照step1,step2,step3里的内容,可以学习如何使用freetype库,大致总结下,为如下步骤。
①初始化:FT_InitFreetype
②加载(打开)字体Face:FT_New_Face
③设置字体大小:FT_Set_Char_Sizes 或 FT_Set_Pixel_Sizes
④选择charmap:FT_Select_Charmap
⑤根据编码值charcode找到glyph : glyph_index = FT_Get_Char_Index(face,charcode)
⑥根据glyph_index取出glyph:FT_Load_Glyph(face,glyph_index)
⑦转为位图:FT_Render_Glyph
⑧移动或旋转:FT_Set_Transform
1.5.2 在LCD上显示一个矢量字体
我们可以参考上图位置的c程序,编写程序。
①初始化freetype库
程序文件:freetype_show_font.c
4872 error = FT_Init_FreeType( &library ); /* initialize library */
②用freetype库中的FT_New_Face函数创建一个face字体文件对象,保存在&face中
程序文件:freetype_show_font.c
4875 error = FT_New_Face( library, argv[1], 0, &face ); /* create face object */
③提取face对象中的glyph,即关键点集
程序文件:freetype_show_font.c
4877 slot = face->glyph;
④设置像素点大小,24*24
程序文件:freetype_show_font.c
4879 FT_Set_Pixel_Sizes(face, 24, 0);
⑤确定坐标
目前我们前面所用的都是LCD的坐标系对应的x与y坐标,然后在freetype上却是使用的笛卡尔坐标系,因此我们还需要转换x与y坐标。
我们将要显示的是‘繁’字,根据上图可知,先计算在lcd坐标系的情况下‘繁’字
的左下角的x坐标与y坐标,因为在笛卡尔坐标中左下角为字符的原点,‘A’是的左上角为整个屏幕的中心点,即(xres/2,yres/2)。
- lcd_x = var.xres/2 + 8 + 16;lcd_y = var.yres/2 + 16
- 则笛卡尔座标系:x = lcd_x = var.xres/2 + 8 + 16 ; y = var.yres - lcd_y = var.yres/2 – 16
- 单位是1/64像素,所以需要乘以64
程序文件:freetype_show_font.c
4888 pen.x = (var.xres/2 + 8 + 16) * 64;
4889 pen.y = (var.yres/2 - 16) * 64;
4890
4891 /* set transformation */
4892 FT_Set_Transform( face, 0, &pen);
⑥找到glyph的位置,然后取出,并转换为位图
4895 error = FT_Load_Char( face, chinese_str[0], FT_LOAD_RENDER );
4896 if (error)
4897 {
4898 printf("FT_Load_Char error\n");
4899 return -1;
4900 }
FT_Load_Char函数调用替代了上图这3步。
最后把转换出来的位图打印出来,也是参考example1.c编写
程序文件:freetype_show_font.c
4902 draw_bitmap( &slot->bitmap,
4903 slot->bitmap_left,
4904 var.yres - slot->bitmap_top);
程序文件:example1.c
修改上图3处位置
-
Width宽度:因为在LCD上显示,宽度自然就是x方向的像素点数,var.xres;
-
Height高度:因为在LCD上显示,高度自然就是y方向的像素点数,var.yres;
-
用点阵实验中的的描点函数lcd_put_pixel替代image数组
lcd_put_pixel(i, j, bitmap->buffer[q * bitmap->width + p]);
⑥编译C程序文件freetype_show_font.c
编译命令:arm-linux-gnueabihf-gcc -finput-charset=GBK -fexec-charset=GBK -o freetype_show_font freetype_show_font.c -lfreetype -lm
⑦将编译好的freetype_show_font的文件与simsun.ttc字体文件拷贝至开发板,simsun.ttc字体文件放在freetype_show_font执行文件的上一层目录下,执行以下命令。
执行命令:./freetype_show_font ../simsun.ttc
如果实验成功,我们将看到屏幕中间会比之前实验多出一个蓝色的‘繁’字。
1.5.3 在LCD上令矢量字体旋转某个角度
在实现显示一个矢量字体后,我们可以添加让该字旋转某个角度的功能。
我们根据输入的第二个参数,判断其旋转角度,主要代码还是参照example1.c
根据上图,增加旋转角度功能,旋转的角度由执行命令的第二个参数指定。
程序文件:freetype_show_font_angle.c
/* use 25 degrees */
4894 angle = ( 1.0 * strtoul(argv[2], NULL, 0) / 360 ) * 3.14159 * 2;
4895 /* set up matrix */
4896 matrix.xx = (FT_Fixed)( cos( angle ) * 0x10000L );
4897 matrix.xy = (FT_Fixed)(-sin( angle ) * 0x10000L );
4898 matrix.yx = (FT_Fixed)( sin( angle ) * 0x10000L );
4899 matrix.yy = (FT_Fixed)( cos( angle ) * 0x10000L );
4900
4901 /* set transformation */
4902 FT_Set_Transform( face, &matrix, &pen);
最后编译,在开发板上运行
编译命令如下:
编译命令:arm-linux-gnueabihf-gcc -finput-charset=GBK -fexec-charset=GBK -o freetype_show_font_angle freetype_show_font_angle.c -lfreetype -lm
编译出的文件名为freetype_show_font_angle,将文件拷贝至开发板
在含有该文件的目录下执行以下命令,以下命令正确执行前提是执行文件freetype_show_font在此目录,而且字体文件simsun.ttc,在上一级目录:
执行命令:./freetype_show_font_angle ../simsun.ttc 90
如果实验成功,我们将看到屏幕中间的蓝色‘繁’字,旋转了90度。
第二章:图像处理
图像处理
2图像处理
前言:所有的图像文件,都是一种二进制格式文件,每一个图像文件,都可以通过解析文件中的每一组二进制数的含义来获得文件中的各种信息,如图像高度,宽度,像素位数等等。只是不同的文件格式所代表的二进制数含义不一样罢了。我们可以通过UltraEdit软件打开图像文件并查看里面的二进制数排列。
2.1 BMP图像处理
2.1.1 BMP文件格式解析
BMP是一种常见的图像格式,BMP文件可看成由4个部分组成:位图文件头(bitmap-file header)、位图信息头(bitmap-information header)、调色板(color palette)和定义位图的字节阵列。以最简单的24位真彩色BMP文件作例子讲解:
- 位图文件头(bitmap-file header)
这部分可以理解为是一个结构体,里面的每一个成员都表示一个属性
位数文件头由以下信息组成:
名称 | 字节数 | 含义 |
---|---|---|
bfType | 2字节 | 表明它是BMP格式的文件, 内容固定为0x42,0x4D, 即ASCII字符中的“B”“M” |
bfSize | 4字节 | BMP文件的大小,单位为字节 |
bfReserved1 | 2字节 | 保留 |
bfReserved2 | 2字节 | 保留 |
我们用UltraEdit打开一个BMP文件,可以看到如下信息
这是该BMP文件前32字节的数据,可以看到,前两个字节分别为0x42,0x4D;
接着后面4个字节依次是0x36,0xF9,0x15,0x00。
在BMP格式中,文件的存储方式是小端模式,即如果一个数据需要用几个字节来表示的话,那么,低位数据存在低位地址上,高位数据存在高位地址上。类似的,还有大端模式,即:如果一个数据需要用几个字节来表示的话,那么,低位数据存在高位地址上,高位数据存在低位地址上。
所以0x36,0xF9,0x15,0x00四个数据拼接方法应该是:0x0015F936(在数字中个位即最右边才是最低位),它正好就是这个文件的大小:
紧接着是4个保留位字节,其数据必须为0x00。
最后是4个字节的便宜位,可以看到位图文件头+位图信息头+调色板的大小应该是0x36。
- 位图信息头(bitmap-information header)
位图信息头也可以理解为是一个结构体,其成员有:
名称 | 字节数 | 含义 |
---|---|---|
biSize | 4 | 整个位图信息头结构体的大小 |
biWidth | 4 | 图像宽度,单位为像素 |
biHeight | 4 | 图像高度,单位为像素。 此外,这个数的 正负可以判断图像是正向还是倒向的,若为 正,则表示是正向;若为负,则表示反向。 其实根本不同就是坐标系的建立方法不一样。 后面写代码时会讲。 |
biPlanes | 2 | 颜色平面书,其值总为1 |
biBitCount | 2 | 即1个像素用多少位的数据来表示,其值可 能为1,4,8,16,24,32。我们是以24位 真彩色为例子讲解的 |
biCompression | 4 | 数据的压缩类型 |
biSizeImage | 4 | 图像数据的大小,单位为字节 |
biXPelsPerMeter | 4 | 水平分辨率,单位是像素/米 |
biYPelsPerMeter | 4 | 垂直分辨率,单位是像素/米 |
biClrUsed | 4 | 调色板中的颜色索引数 |
biClrImportant | 4 | 说明有对图像有重要影响的颜色索引的数 目,若为0,表示都重要 |
对照源文件数据:
0E-11:00000028h = 40,表示这个结构体大小是40字节。
12-15:00000320h = 800,图像宽为800像素。
16-19:00000258h = 600,图像高为600像素,与文件属性一致。这是一个正数,说明图像是正向的,数据是以图像左下角为原点,以水平向右为X轴正方向,以垂直向上为Y轴正方向排列的。若为负,则说明图像是反向的,数据是以图像左上角角为原点,以水平向右为X轴正方向,以垂直向下为Y轴正方向排列的。
1A-1B:0001h, 该值总为1。
1C-1D:0018h = 24, 表示每个像素占24个比特,即24位真彩色
上面这几个信息跟文件属性是一致的:
1E-21:00000000h,BI_RGB, 说明本图像不压缩。
22-25:00000000h,图像的大小,因为使用BI_RGB,所以设置为0。
26-29:00000000h,水平分辨率,缺省。
2A-2D:00000000h,垂直分辨率,缺省。
2E-31:00000000h,对于24位真彩色来说,是没有调色板的,所以为0。
32-35:00000000h,对于24位真彩色来说,是没有调色板的,所以为0。
- 调色板(color palette)
24位真彩色没有调色板,这里为了简化不赘诉。
- 定义位图的字节阵列
这一部分就是真正的图像数据了,24位真彩色数据是按按BGR各一字节循环排列而成。
2.1.2 代码实现:将BMP文件解析为RGB格式,在LCD上显示
让BMP文件在开发板的LCD上显示出来,有几个需要注意的点:
-
开发板LCD上的显示格式是RGB格式的,而且有多种表示格式:可能用2字节表示(RGB565格式),可能用3字节表示(RGB888),而原始的24位真彩色BMP文件则是按BGR格式排列的,需要对原始的图像数据进行转化。
-
在转化过程中,LCD上的显存地址固定是以LCD左上角为首地址,而BMP格式中正向图像是以图片的左下角为数据首地址的。因此在进行数据转化时还需要注意坐标的变换。
代码清单2.1实现了将24位真彩色的BMP图像转化为RGB格式
代码清单2.1
1. /**********************************************************************
2. * 函数名称: IsBmp
3. * 功能描述: 判断该文件是否为BMP文件
4. * 输入参数: ptFileMap - 内含文件信息
5. * 输出参数: 无
6. * 返 回 值: 0 - 是BMP格式, -1 -不是BMP格式
7. ***********************************************************************/
8. int IsBmp(FILE **ppFp, const char *strFileName)
9. {
10. char strCheckHeader[2];
11. *ppFp= fopen(strFileName, "rb+");
12. if (*ppFp== NULL) {
13. return -1;
14. }
15. if (fread(strCheckHeader, 1, 2, *ppFp) != 2)
16. return -1;
17.
18. if (strCheckHeader[0] != 0x42 || strCheckHeader[1] != 0x4d)
19. return -1;
20. else
21. return 0;
22. }
23.
24.
25.
26. /**********************************************************************
27. * 函数名称: MapFile
28. * 功能描述: 使用mmap函数映射一个文件到内存,以后就可以直接通过内存来访问文件
29. * 输入参数: PT_PictureData ptData 内含图像数据
30. * 输出参数: ptData->iFileSize : 文件大小
31. * ptData->pucFileData : 映射内存的首地址
32. * 返 回 值: 0 - 成功其他值 - 失败
33. ***********************************************************************/
34. int MapFile(PT_PictureData ptData)
35. {
36. int iFd;
37. struct stat tStat;
38.
39. /* 打开文件 */
40. iFd = fileno(ptData->ptFp);
41. fstat(iFd, &tStat);
42. ptData->iFileSize= tStat.st_size;
43. ptData->pucFileData= (unsigned char *)mmap(NULL , tStat.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, iFd, 0);
44. if (ptData->pucFileData == (unsigned char *)-1)
45. {
46. printf("mmap error!\n");
47. return -1;
48. }
49. return 0;
50. }
51.
52. /**********************************************************************
53. * 函数名称: DecodeBmp2Rgb
54. * 功能描述:把BMP文件转化为rgb格式
55. * 输入参数: strFileName - 文件名
56. * ptData - 内含图像信息
57. * 返 回 值: 0 - 成功其他值 - 失败
58. * -1 - 文件不是BMP格式
59. * -2 - 不支持的bpp
60. * -3 - 图像缓存区分配失败
61. ***********************************************************************/
62. static int DecodeBmp2Rgb(const char *strFileName, PT_PictureData ptData) {
63. int x,y;
64. int iPos = 0;
65. int iLineWidthAlign;
66. BITMAPFILEHEADER *ptBITMAPFILEHEADER;
67. BITMAPINFOHEADER *ptBITMAPINFOHEADER;
68. unsigned char *aFileHead;
69. unsigned char *pucSrc;
70. unsigned char *pucDest;
71. int iLineBytes;
72.
73. /* 判断该文件是否为BMP格式 */
74. if (IsBmp(&ptData->ptFp, strFileName))
75. return -1;
76.
77. /* 将BMP文件映射到内存空间 */
78. MapFile(ptData);
79.
80.
81. aFileHead = ptData->pucFileData;
82.
83. ptBITMAPFILEHEADER = (BITMAPFILEHEADER *)aFileHead;
84. ptBITMAPINFOHEADER = (BITMAPINFOHEADER *)(aFileHead + sizeof(BITMAPFILEHEADER));
85. /* 获取必要的图像信息 */
86. ptData->iWidth = ptBITMAPINFOHEADER->biWidth;
87. ptData->iHeight = ptBITMAPINFOHEADER->biHeight;
88. ptData->iBpp = ptBITMAPINFOHEADER->biBitCount;
89. iLineBytes = ptData->iWidth*ptData->iBpp/8;//一行数据的字节数
90. ptData->iBmpDataSize= ptData->iHeight * iLineBytes;//整个BMP图像的字节数
91. /*暂时只支持24bpp格式*/
92. if (ptData->iBpp != 24)
93. {
94. printf("iBMPBpp = %d\n", ptData->iBpp);
95. printf("sizeof(BITMAPFILEHEADER) = %d\n", sizeof(BITMAPFILEHEADER));
96. return -2;
97. }
98.
99. /* 分配空间 */
100. ptData->pucBmpData = malloc(ptData->iBmpDataSize);
101. ptData->pucRgbData = malloc(ptData->iBmpDataSize);
102.
103. if (NULL == ptData->pucBmpData||NULL == ptData->pucRgbData)
104. return -2;
105.
106. /* 从bmp文件中读取图像信息,24bpp的BMP图像为BGR格式 */
107. pucDest = ptData->pucBmpData;
108. iLineWidthAlign = (iLineBytes + 3) & ~0x3; /* 向4取整 */
109. pucSrc = aFileHead + ptBITMAPFILEHEADER->bfOffBits;
110.
111. pucSrc = pucSrc + (ptData->iHeight - 1) * iLineWidthAlign;
112.
113. /* 对于bmp文件中的源数据,是以左下角为原点计算坐标的,因此拷贝数据时需要转换坐标 */
114. for (y = 0; y < ptData->iHeight; y++)
115. {
116. memcpy(pucDest, pucSrc, ptData->iWidth*3);
117. pucSrc -= iLineWidthAlign;
118. pucDest += iLineBytes;
119. }
120.
121.
122. /* 将得到的BGR数据转化为RGB数据 */
123. for (y = 0; y < ptData->iHeight; y++){
124. for(x = 0;x<ptData->iWidth*3;x+=3){
125. ptData->pucRgbData[iPos++] = ptData->pucBmpData[y*ptData->iWidth*3+x+2];
126. ptData->pucRgbData[iPos++] = ptData->pucBmpData[y*ptData->iWidth*3+x+1];
127. ptData->pucRgbData[iPos++] = ptData->pucBmpData[y*ptData->iWidth*3+x+0];
128. }
129. }
130.
131. return 0;
132.
133. }
2.2 JPEG图像处理
2.2.1 JPEG文件格式和libjpeg编译
JPEG的后缀名为.jpg的图像文件。对于图像内容和信息相同的JPEG文件和BMP文件,JPEG格式的文件要比BMP格式的文件小得多,这是因为JPEG文件是经过JPEG压缩算法后得到的一种文件格式。
相对于BMP格式的文件,JPEG由于压缩算法的关系,其文件解析较为复杂,我们可以利用Linux系统开源的优点,使用开源工具对jpeg文件进行格式的解析和转换。
我们可以使用libjpeg库来对jpeg文件进行格式的解析和转换。libjpeg支持X86,ARM等架构。libjpeg是开源工具,所以可以在网上免费下载。
在使用libjpeg之前,我们先要交叉编译libjpeg的库文件和头文件并存到开发板的文件系统中。以下是libjpeg的编译过程:
- 解压并进入文件目录
tar xzf libjpeg-turbo-1.2.1.tar.gz
cd libjpeg-turbo-1.2.1/
- 交叉编译
tar xzf libjpeg-turbo-1.2.1.tar.gz
./configure --prefix=/work/projects/libjpeg-turbo-1.2.1/tmp/ --host=arm-linux
make
make install
- 将编译出来的头文件和库文件拷贝到交叉编译器的相应目录下
cd /work/projects/libjpeg-turbo-1.2.1/tmp/include
cp * /usr/local/arm/4.3.2/arm-none-linux-gnueabi/libc/usr/include
cd /work/projects/libjpeg-turbo-1.2.1/tmp/lib
cp *so* -d /usr/local/arm/4.3.2/arm-none-linux-gnueabi/libc/armv4t/lib
- 将编译出来的头文件和库文件拷贝到开发板文件系统的相应目录下
cd /work/projects/libjpeg-turbo-1.2.1/tmp/lib
cp *.so* /work/nfs_root/fs_mini_mdev_new/lib/ -d
2.2.2 libjpeg接口函数的解析和使用
libjpeg的使用方法可以参考解压包中的使用说明libjpeg.txt和例程example.c。libjpeg的使用步骤简单总结如下:
1. 分配和初始化一个jpeg_compress_struct结构体
cinfo.err = jpeg_std_error(&jerr);
jpeg_create_decompress(&cinfo);
2. 指定源文件
jpeg_stdio_src(&cinfo, infile);
参数1是步骤1中分配的jpeg_compress_struct类型的结构体
参数2是要解析的JPEG文件的文件句柄。
3. 获得jpg信息头并设置解压参数
jpeg_read_header(&cinfo, TRUE);
当调用完这个参数之后,我们就可以通过cinfo中的image_width,image_height等成员来获得图像的信息了。此外我们还可以设置cinfo中的scale_num和scale_denom等成员变量来设置解压参数。
4. 启动解压
jpeg_start_decompress(&cinfo);
调用这个函数后,就可以对cinfo所指定的源文件进行解压,并将解压后的数据存到cinfo结构体的成员变量中。
5. 读取解压后数据
jpeg_read_scanlines(&cinfo, buffer, 1);
调用这个函数后,可以读取RGB数据到buffer中,参数3能指定读取多少行
6. 完成读取
jpeg_finish_decompress(&cinfo);
7. 释放jpeg_compress_struct结构体
jpeg_destroy_decompress(&cinfo);
完成读取后释放结构体
2.2.3 使用libjpeg把JPEG文件解析为RGB格式,在LCD上显示
根据上节的解析,利用上述的库函数将JPEG文件解析为RGB格式了。
代码清单2.2
1. /**********************************************************************
2. * 函数名称: IsJpg
3. * 功能描述:判断是否为Jpg文件
4. * 输入参数: ptData - 内含图像信息
5. strFileName - 文件名
6. * 返 回 值:0 - 不是JPG格式 其他-是JPG格式
7. ***********************************************************************/
8. static int IsJpg(PT_PictureData ptData, const char *strFileName)
9. {
10. int iRet;
11.
12. jpeg_stdio_src(&ptData->tInfo, ptData->ptFp);
13.
14. /* 用jpeg_read_header获得jpeg信息*/
15. iRet = jpeg_read_header(&ptData->tInfo, TRUE);
16.
17. return (iRet == JPEG_HEADER_OK);
18. }
19.
20. /**********************************************************************
21. * 函数名称: DecodeJpg2Rgb
22. * 功能描述:把JPG文件解析为RGB888格式
23. * 输入参数: ptData - 内含文件信息
24. * strFileName - 文件名
25. * 输出参数:PT_PictureData->pucRgbData - 内含rgb数据
26. * 返 回 值:0 - 成功 其他-失败
27. ***********************************************************************/
28. static int DecodeJpg2Rgb(const char *strFileName, PT_PictureData ptData){
29. int iRowSize;
30. unsigned char *pucbuffer;
31. unsigned char *pucHelp;//辅助拷贝变量
32.
33. /* 1.分配和初始化一个jpeg_compress_struct结构体 */
34. ptData->tInfo.err = jpeg_std_error(&ptData->tJerr);
35. jpeg_create_decompress(&ptData->tInfo);
36.
37.
38. /* 2.指定源文件*/
39. if ((ptData->ptFp= fopen(strFileName, "rb")) == NULL) {
40. fprintf(stderr, "can't open %s\n", strFileName);
41. return -1;
42. }
43.
44. /* 3.获得jpg信息头并设置解压参数并判断是否为JPEG格式文件 */
45. if (!IsJpg(ptData, strFileName)) {
46. printf("file is not jpg ...\n");
47. return -1;
48. }
49.
50.
51.
52. /* 默认尺寸为原尺寸 */
53. ptData->tInfo.scale_num = 1;
54. ptData->tInfo.scale_denom = 1;
55. /* 4. 启动解压:jpeg_start_decompress */
56. jpeg_start_decompress(&ptData->tInfo);
57.
58.
59. /* 解压完成后可以通过tInfo中的成员获得图像的某些信息 */
60. ptData->iWidth= ptData->tInfo.output_width;
61. ptData->iHeight = ptData->tInfo.output_height;
62. ptData->iBpp = ptData->tInfo.output_components*8;
63. /* 计算一行的数据长度 */
64. iRowSize = ptData->iWidth * ptData->tInfo.output_components;
65. pucbuffer = malloc(iRowSize);
66. ptData->iRgbSize= iRowSize * ptData->iHeight;
67. ptData->pucRgbData = malloc(ptData->iRgbSize);
68.
69. /* pucHelp指向ptData->pucRgbData首地址 */
70. pucHelp = ptData->pucRgbData;
71. /* 5.循环调用jpeg_read_scanlines来一行一行地获得解压的数据 */
72. while (ptData->tInfo.output_scanline < ptData->tInfo.output_height)
73. {
74. /* 调用jpeg_read_scanlines得到的时候会存到pucbuffer中 */
75. jpeg_read_scanlines(&ptData->tInfo, &pucbuffer, 1);
76. /* 将数据一行行读到缓冲区中 */
77. memcpy(pucHelp,pucbuffer,iRowSize);
78. pucHelp += iRowSize;
79. }
80. free(pucbuffer);
81. /* 6.完成读取 */
82. jpeg_finish_decompress(&ptData->tInfo);
83. /* 7.释放jpeg_compress_struct结构体 */
84. jpeg_destroy_decompress(&ptData->tInfo);
85. return 0;
86. }
2.3 PNG图像处理
2.3.1 PNG文件格式和libpng编译
跟JPEG文件格式一样,PNG也是一种使用了算法压缩后的图像格式,与JPEG不同,PNG使用从LZ77派生的无损数据压缩算法。对于PNG文件格式,也有相应的开源工具libpng。
libpng库可从官网上下载最新的源代码:
http://www.libpng.org/pub/png/libpng.html
在使用libpng之前,我们先要交叉编译libpng的库文件和头文件并存到开发板的文件系统中。以下是libpng的编译过程:
- 解压并进入文件目录
tar xzf libpng-1.6.37.tar.gz
cd libpng-1.6.37/
- 交叉编译
./configure --prefix=/work/projects/libpng-1.6.37/tmp/ --host=arm-linux
make
make install
- 将编译出来的头文件和库文件拷贝到交叉编译器的相应目录下
cd /work/projects/libpng-1.6.37/tmp/include
cp * /usr/local/arm/4.3.2/arm-none-linux-gnueabi/libc/usr/include
cd /work/projects/libpng-1.6.37/tmp/lib
cp *so* -d /usr/local/arm/4.3.2/arm-none-linux-gnueabi/libc/armv4t/lib
- 将编译出来的头文件和库文件拷贝到开发板文件系统的相应目录下
cd /work/projects/libpng-1.6.37/tmp/lib
cp *.so* /work/nfs_root/fs_mini_mdev_new/lib/ -d
2.3.2 libpng接口函数的解析和使用
libpng的使用方法可以参考解压包中的使用说明libpng-manual.txt和例程example.c。libjpeg的使用步骤简单总结如下:
-
分配和初始化两个与libpng相关的结构体png_ptr,info_ptr
A. png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
参数2,3,4分别是用户自定义的错误处理函数,若无,则填NULL。
B. info_ptr = png_create_info_struct(png_ptr);
-
设置错误返回点
setjmp(png_jmpbuf(png_ptr));
当出现错误时,libpng将会自动调用返回到这个点。在这个点我们可以进行一些清理工作。如果在调用png_create_read_struct时没有设置自定义的错误处理函数,这一步是必须要做的。
-
指定源文件
png_init_io(png_ptr, fp);
参数1是步骤1中分配的png_ptr结构体,参数2是需要解析的PNG文件的文件句柄。
-
获取PNG图像的信息
A. 解析图片数据信息
png_read_png(png_ptr, info_ptr, png_transforms, png_voidp_NULL);
该函数会把所有的图片数据解码到info_ptr数据结构中。至于转化为什么格式,由参数png_transforms决定,它是一个整型参数,可以使用libpng库中定义的宏进行传参。这个参数相关的宏有很多,具体的可以参考库中的相关文件的解析。
B.查询图像信息
此外,我们还可以通过png_get_image_width,png_get_image_height,png_get_color_type等函数获得png图像的宽度,高度,颜色类型等信息,更多的图像信息获取函数可以在文件pngget.c中找到。
-
将info_ptr中的图像数据读取出来
有两种读取PNG图像信息的方法:
A. 一次性把所有的数据读入内存
png_read_image(png_ptr, row_pointers);
参数1是步骤1中分配的png_ptr,参数2是存放图片数据的指针。
B. 也可以逐行读取
row_pointers = png_get_rows(png_ptr, info_ptr);
参数1和参数2分别是步骤1中分配的png_ptr, info_ptr,返回值是每行数据的首地址。
参数1是步骤1中分配的png_ptr,参数2是存放图片数据的指针。
-
销毁内存
png_destroy_read_struct(&png_ptr, &info_ptr, 0);
2.3.3 使用libpng把png文件转为rgb格式,在LCD上显示
代码清单2.3
1. /**********************************************************************
2. * 函数名称: IsnotPng
3. * 功能描述:判断是否为PNG文件
4. * 输入参数: ppFp - 文件句柄指针
5. strFileName - 文件名
6. * 返 回 值:0 - 是PNG格式 其他-不是PNG格式
7. ***********************************************************************/
8. int IsnotPng(FILE **ppFp, const char *strFileName)
9. {
10. char strCheckHeader[8];
11. *ppFp= fopen(strFileName, "rb");
12. if (*ppFp== NULL) {
13. return -1;
14. }
15. /* 读取PNG文件前8个字节,使用库函数png_sig_cmp即可判断是否为PNG格式 */
16. if (fread(strCheckHeader, 1, 8, *ppFp) != 8)
17. return -1;
18. return png_sig_cmp(strCheckHeader, 0, 8);
19.
20. }
21.
22. /**********************************************************************
23. * 函数名称: DecodePng2Rgb
24. * 功能描述:把PNG文件解析为RGB888格式
25. * 输入参数: ptData - 内含文件信息
26. * strFileName - 文件名
27. * 输出参数:PT_PictureData->pucRgbData - 内含rgb数据
28. * 返 回 值:0 - 成功 其他-失败
29. ***********************************************************************/
30. static int DecodePng2Rgb(const char *strFileName, PT_PictureData ptData)
31. {
32. int i, j;
33. int iPos = 0;
34. png_bytepp pucPngData;
35. /* 0.判断该文件是否为PNG格式 */
36. if (IsnotPng(&ptData->ptFp, strFileName)) {
37. printf("file is not png ...\n");
38. return -1;
39. }
40.
41. /* 1.分配和初始化两个与libpng相关的结构体png_ptr,info_ptr */
42. ptData->ptPngStrPoint = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
43. ptData->ptPngInfoPoint= png_create_info_struct(ptData->ptPngStrPoint);
44.
45. /* 2.设置错误的返回点 */
46. setjmp(png_jmpbuf(ptData->ptPngStrPoint));
47. rewind(ptData->ptFp); //等价fseek(fp, 0, SEEK_SET);
48.
49. /* 3.指定源文件 */
50. png_init_io(ptData->ptPngStrPoint, ptData->ptFp);
51.
52. /* 4.获取PNG图像数据信息和通道数,宽度,高度等
53. * 使用PNG_TRANSFORM_EXPAND宏做参数的作用是根据通道数的不同,
54. * 将PNG图像转换为BGR888或ABGR8888格式*/
55. png_read_png(ptData->ptPngStrPoint, ptData->ptPngInfoPoint, PNG_TRANSFORM_EXPAND, 0);
56. ptData->iChannels = png_get_channels(ptData->ptPngStrPoint, ptData->ptPngInfoPoint);
57. ptData->iWidth = png_get_image_width(ptData->ptPngStrPoint, ptData->ptPngInfoPoint);
58. ptData->iHeight = png_get_image_height(ptData->ptPngStrPoint, ptData->ptPngInfoPoint);
59.
60.
61. /* 5.将info_ptr中的图像数据读取出来 */
62. pucPngData = png_get_rows(ptData->ptPngStrPoint, ptData->ptPngInfoPoint); //也可以分别每一行获取png_get_rowbytes();
63. if (ptData->iChannels == 4) { //判断是24位还是32位
64. ptData->iRawSize= ptData->iWidth * ptData->iHeight*4; //申请内存先计算空间
65. ptData->pucRawData= (unsigned char*)malloc(ptData->iRawSize);
66. if (NULL == ptData->pucRawData) {
67. printf("malloc rgba faile ...\n");
68. png_destroy_read_struct(&ptData->ptPngStrPoint, &ptData->ptPngInfoPoint, 0);
69. fclose(ptData->ptFp);
70. return -1;
71. }
72. /* 从pucPngData里读出实际的RGBA数据出来
73. * 源数据为ABGR格式*/
74. for (i = 0; i < ptData->iHeight; i++)
75. for (j = 0; j < ptData->iWidth * 4; j += 4) {
76. ptData->pucRawData[iPos++] = pucPngData[i][j + 3];
77. ptData->pucRawData[iPos++] = pucPngData[i][j + 2];
78. ptData->pucRawData[iPos++] = pucPngData[i][j + 1];
79. ptData->pucRawData[iPos++] = pucPngData[i][j + 0];
80. }
81.
82. /* 将得到的RGBA转换为RGB888格式 */
83. if(RgbaToRgb(ptData)!=0)
84. return -1;
85.
86. }
87. else if (ptData->iChannels == 3 ) { //判断颜色深度是24位还是32位
88. ptData->iRgbSize= ptData->iWidth * ptData->iHeight*3; //申请内存先计算空间
89. ptData->pucRgbData = (unsigned char*)malloc(ptData->iRgbSize);
90. if (NULL == ptData->pucRgbData) {
91. printf("malloc rgba faile ...\n");
92. png_destroy_read_struct(&ptData->ptPngStrPoint, &ptData->ptPngInfoPoint, 0);
93. fclose(ptData->ptFp);
94. return -1;
95. }
96. /* 从pucPngData里读出实际的RGB数据
97. * 源数据为BGR格式*/
98. for (i = 0; i < ptData->iHeight; i ++) {
99. for (j = 0; j < ptData->iWidth*3; j += 3) {
100. ptData->pucRgbData[iPos++] = pucPngData[i][j+2];
101. ptData->pucRgbData[iPos++] = pucPngData[i][j+1];
102. ptData->pucRgbData[iPos++] = pucPngData[i][j+0];
103. }
104. }
105. ptData->iBpp = 24;//转化之后的格式为RGB888格式
106. }
107. else return -1;
108.
109.
110. /* 6:销毁内存 */
111. png_destroy_read_struct(&ptData->ptPngStrPoint, &ptData->ptPngInfoPoint, 0);
112. fclose(ptData->ptFp);
113.
114.
115. return 0;
116. }
2.4 图像调整
2.4.1 图像的缩放
2.4.1.1 图像缩放算法浅析
图像缩放算法有很多种,这里参考网友"lantianyu520"所著的"图像缩放算法"。
原理浅析
要理解这个图像缩放算法的原理,最重要的是需要理解:对于图像上的每一个像素点,它缩放前后,相对于整个图像的比例应该是一样的。
比如:
以一个长度和宽度分别为200,100的长方形为例,将其放大两倍,那么缩放后的长度和宽度为400,200。
为方便理解,我们建立一个笛卡尔坐标系,把这个长方形左下角的顶点放到坐标(0,0)位置,四个点的坐标分别为:(0,0),(0,100),(200,0),(200,100)。
假设此时对长方形中的坐标点(40,50),它的x坐标相对于长的比值是40/200=0.2,y坐标相对于宽的比值是50/100=0.5,那么该点的变换后的坐标Dx,Dy则应满足:Dx/400 = 5;Dy/200 = 0.5,这样,缩放后的坐标就可以算出来了。
根据上面的分析,设缩放前的像素点坐标为(Sx,Sy),对应的缩放后的像素点坐标为(Dx,Dy),缩放前的图像长宽分别为Sw,Sh,缩放后的图像长宽分别为Dw,Dh,则有:
Sx/Dx = Sw/Dw,Sy/Dy = Sh/Dh
故有Sx = Dx * Sw/Dw,Sy = Dy * Sh/Dh,
2.4.1.2源码编写:图像缩放算法
有了这个上面两条等式后,图像缩放算法的代码就好理解了。
下面的函数实现了基于上述原理实现的图像缩放算法:
代码清单2.4
1. /**********************************************************************
2. * 函数名称: PicZoom
3. * 功能描述: 近邻取样插值方法缩放图片
4. * 注意该函数会分配内存来存放缩放后的图片,用完后要用free函数释放掉
5. * "近邻取样插值"的原理请参考网友"lantianyu520"所著的"图像缩放算法"
6. * 输入参数: ptPicData - 内含缩放前后的图像数据
7. * fSize - 缩放倍数
8. * 输出参数: ptPicData->pucZoomData,内含缩放后的数据
9. * 返 回 值: 0 - 成功, 其他值 - 失败
10. ***********************************************************************/
11. int PicZoom(PT_PictureData ptPicData,float fSize)
12. {
13. ptPicData->iZoomWidth = ptPicData->iWidth * fSize;
14. ptPicData->iZoomHeight= ptPicData->iHeight* fSize;
15. unsigned long* pdwSrcXTable;
16. unsigned long x;
17. unsigned long y;
18. unsigned long dwSrcY;
19. unsigned char *pucDest;
20. unsigned char *pucSrc;
21. unsigned long dwPixelBytes = ptPicData->iBpp/8;
22. ptPicData->pucZoomData= malloc(sizeof(unsigned char) * ptPicData->iZoomWidth*ptPicData->iZoomHeight*ptPicData->iBpp/8);
23. pdwSrcXTable = malloc(sizeof(unsigned long) * ptPicData->iZoomWidth);
24. if (NULL == pdwSrcXTable){
25. printf("malloc error!\n");
26. return -1;
27. }
28.
29. /* 这几个for循环的本质是Sx = Dx * Sw/Dw,Sy = Dy * Sh/Dh*/
30. for (x = 0; x < ptPicData->iZoomWidth; x++){//生成表 pdwSrcXTable
31. /* 第一个for循环对应x方向的坐标
32. * pdwSrcXTable[x] 对应Sx,
33. * x 对应Dx,
34. * ptPicData->iWidth 对应Sw
35. * ptPicData->iZoomWidth 对应 Dw*/
36. pdwSrcXTable[x]=(x*ptPicData->iWidth/ptPicData->iZoomWidth);
37. }
38.
39. for (y = 0; y < ptPicData->iZoomHeight; y++){
40. /* 第2个循环对应y方向的坐标
41. * dwSrcY 对应Sy,
42. * y 对应Dy,
43. * ptPicData->iHeight 对应Sh
44. * ptPicData->iZoomHeight 对应 Dh*/
45. dwSrcY = (y * ptPicData->iHeight / ptPicData->iZoomHeight);
46. /* 根据这些可算得各像素点的RGB数据存放的地址 */
47. pucDest = ptPicData->pucZoomData + y*ptPicData->iZoomWidth*3;
48. pucSrc = ptPicData->pucRgbData + dwSrcY*ptPicData->iWidth*3;
49.
50. /* 最后拷贝数据 */
51. for (x = 0; x <ptPicData->iZoomWidth; x++){
52. memcpy(pucDest+x*dwPixelBytes, pucSrc+pdwSrcXTable[x]*dwPixelBytes, dwPixelBytes);
53. }
54. }
55.
56. free(pdwSrcXTable);
57. return 0;
58. }
2.4.2 图像的旋转
2.4.2.1 图像旋转算法浅析
这里的图像旋转算法原理参考网友"落叶的思维"所著的"图像旋转算法与实现"
原理浅析
这个旋转算法的原理的关键点有两个:
- 原图像是以图像的左下角为原点建立笛卡尔坐标系的,而旋转一般是以图像的中心作为旋转点旋转的。
因此为了便于转换,我们先约定两个坐标系,一个是以图像左下角为原点建立的坐标系,称为坐标系A,这也是原图像的坐标系。一个是以图像中心为原点建立的坐标系,称为坐标系B。
由此,可以知道这个旋转算法的步骤:先将坐标系A下的坐标转换为坐标系B下的坐标,然后在坐标系B下进行图像的旋转。
在坐标系B下,我们假设点(x0,y0)距离原点的距离为r,点与原点之间的连线与x轴的夹角为b,旋转的角度为a,旋转后的点为(x1,y1), 如下图所示。
那么有以下结论:
x0=rcosb;y0=rsinb
x1 = rcos(b-a) = rcosbcosa+rsinbsina=x0cosa+y0sina;
y1=rsin(b-a)=rsinbcosa-rcosbsina=-x0sina+y0cosa;
最后,由于我们显示图像的RGB数据还是要在坐标系A下获取的,我们最后只需要将坐标系B下的x1,y1转换回坐标系A下的坐标就可以了。
旋转后的图像的长和宽会发生变化,因此要计算新图像的长和宽。
由几何关系可知,新图像的长和宽分别是旋转后,对角坐标相见后的最大值
2.4.2.2 源码编写:图像旋转算法
代码清单2.5
1. #define PI 3.1415926535
2. //角度到弧度转化
3. #define RADIAN(angle) ((angle)*PI/180.0)
4.
5.
6.
7.
8.
9. typedef struct ConcernCoor {
10. int iLTx;// left top x
11. int iLTy;//left top y
12. int iLBx;//left bottom x
13. int iLBy;//left bottom y
14. int iRTx;//right top x
15. int iRTy;//right top y
16. int iRBx;// right bottom x
17. int iRBy;// right bottom y
18. }T_ConcernCoor, *PT_ConcernCoor;
19.
20.
21. /**********************************************************************
22. * 函数名称: max
23. * 功能描述:比较两个参数,返回较大值
24. * 输入参数:x,y均为int型
25. * 输出参数: 无
26. * 返 回 值: x,y中的较大值
27. ***********************************************************************/
28. static int max(int x,int y){
29. return x>y?x:y;
30. }
31. /**********************************************************************
32. * 函数名称: PicRotate
33. * 功能描述: 旋转图片
34. * 注意该函数会分配内存来存放缩放后的图片,用完后要用free函数释放掉
35. * 参考网友"落叶的思维"所著的"图像旋转算法与实现"
36. * 输入参数: ptPicData - 内含图片的象素数据
37. * fAngle - 旋转角度,0<=angle<=360
38. * 输出参数: ptPicData->pucRotateData,内含旋转后的rgb数据
39. * 返 回 值: 0 - 成功, 其他值 - 失败
40. ***********************************************************************/
41. int PicRotate(PT_PictureData ptPicData,float fAngle)
42. {
43. int i ,j;
44. T_ConcernCoor tConCor,tRonCor;
45. //原图像每一行去除偏移量的字节数
46. //int iSrcLineSize = bitCount * srcW / 8;
47. int iSrcLineSize = ptPicData->iBpp* ptPicData->iZoomWidth / 8;
48. int iDesLineSize;
49. int iX;//旋转后的x坐标
50. int iY; //旋转后的y坐标
51.
52. /* 将坐标系A下的坐标转换为坐标系B下的坐标,
53. * 用于计算旋转后的图像的宽和高
54. * tConCor用于存放坐标系B下旋转前的坐标
55. * tRonCor用于存放坐标系B下旋转后的坐标*/
56. tConCor.iLTx = -ptPicData->iZoomWidth/2; tConCor.iLTy = ptPicData->iZoomHeight/2;
57. tConCor.iRTx = ptPicData->iZoomWidth/2; tConCor.iRTy = ptPicData->iZoomHeight/2;
58. tConCor.iLBx = -ptPicData->iZoomWidth/2;tConCor.iLBy = -ptPicData->iZoomHeight/2;
59. tConCor.iRBx = ptPicData->iZoomWidth/2;tConCor.iRBy = -ptPicData->iZoomHeight/2;
60.
61.
62. /* 计算坐标系B下旋转后的坐标 */
63. double sina = sin(RADIAN(fAngle));
64. double cosa = cos(RADIAN(fAngle));
65. tRonCor.iLTx =tConCor.iLTx * cosa + tConCor.iLTy * sina;
66. tRonCor.iLTy = -tConCor.iLTx * sina + tConCor.iLTy * cosa;
67. tRonCor.iRTx =tConCor.iRTx * cosa + tConCor.iRTy * sina;
68. tRonCor.iRTy = -tConCor.iRTx * sina + tConCor.iRTy * cosa;
69. tRonCor.iLBx = tConCor.iLBx * cosa + tConCor.iLBy * sina;
70. tRonCor.iLBy = -tConCor.iLBx * sina + tConCor.iLBy * cosa;
71. tRonCor.iRBx = tConCor.iRBx * cosa + tConCor.iRBy * sina;
72. tRonCor.iRBy = -tConCor.iRBx * sina + tConCor.iRBy * cosa;
73.
74.
75. /* 计算旋转后图像宽和高 */
76. ptPicData->iRotateWidth = max(abs(tRonCor.iRBx - tRonCor.iLTx),abs(tRonCor.iRTx - tRonCor.iLBx));
77. ptPicData->iRotateHeight = max(abs(tRonCor.iRBy - tRonCor.iLTy),abs(tRonCor.iRTy - tRonCor.iLBy));
78.
79. /* 像素信息要保证3字节对齐,否则数据有可能出错*/
80. iDesLineSize = ((ptPicData->iRotateWidth* ptPicData->iBpp+ 23) / 24) * 3 ;
81. /* 分配旋转后的空间,注意这里要用旋转后的宽和高 */
82. ptPicData->pucRotateData = malloc(iDesLineSize * ptPicData->iRotateHeight);
83. if(NULL == ptPicData->pucRotateData){
84. printf("malloc error\n");
85. return -1;
86. }
87.
88. /* 通过新图像的坐标,计算对应的原图像的坐标*
89. * i,j坐标就是对应的坐标系B下的x1,y1*/
90. for (i = 0; i < ptPicData->iRotateHeight; i++){
91. for (j = 0; j < ptPicData->iRotateWidth; j++){
92. /* 坐标系B下的x,y1坐标,经过逆运算转换得到iX,iY,这两个值对应x0,y0 */
93. iX = (j - ptPicData->iRotateWidth / 2)*cos(RADIAN(360 - fAngle)) + (-i + ptPicData->iRotateHeight / 2)*sin(RADIAN(360 - fAngle));
94. iY = -(j - ptPicData->iRotateWidth / 2)*sin(RADIAN(360 - fAngle)) + (-i + ptPicData->iRotateHeight / 2)*cos(RADIAN(360 - fAngle));
95. /*如果这个坐标不在原图像内,则不赋值*/
96. if (iX > ptPicData->iZoomWidth / 2 || iX < -ptPicData->iZoomWidth / 2 || iY > ptPicData->iZoomHeight / 2 || iY < -ptPicData->iZoomHeight / 2){
97. continue;
98. }
99. /* 再将坐标系B下的x0,y0坐标,转换为坐标系A下的坐标 */
100. int iXN = iX + ptPicData->iZoomWidth / 2;
101. int iYN = abs(iY - ptPicData->iZoomHeight / 2);
102. /* 值拷贝*/
103. memcpy(&ptPicData->pucRotateData[i * iDesLineSize + j * 3],&ptPicData->pucZoomData[iYN * iSrcLineSize + iXN * 3],3);
104. }
105. }
106. return 0;
107. }
第三章:输入系统
输入系统
3 输入系统
3.1 什么是输入系统?
在了解输入系统之前,先来了解什么是输入设备?常见的输入设备有键盘、鼠标、遥控杆、书写板、触摸屏等等,用户通过这些输入设备与Linux系统进行数据交换,Linux系统为了统一管控和处理这些设备,于是就实现了一套固定的与硬件无关的输入系统框架,供用户空间程序使用,这就是输入系统。
3.2 输入系统应用框架描述
在Linux输入系统中,主要分三层进行管理,分别是input core(输入系统核心层)、drivers(输入系统驱动层)以及event handlers(输入系统事件层),如下图所示,这就是Linux输入系统的基本框架:
举个非常简单的例子,比如用户按下键盘里的其中一个按键,它遵循流程是这样的:
按键按下-->输入系统驱动层-->输入系统核心层-->输入系统事件层--->用户空间
对于应用程序软件编程的角度,我们只需要关注用户空间是怎么得到按键按下以后获取的是什么事件就可以了,例如我想知道我当前按下的按是短按还是长按?或者我想知道当前我按下键盘的是空格键还是回车键等等。
3.3 输入系统事件的读取与分析
用户空间的设备节点那么多,怎么知道当前是哪个设备上报的呢?例如想知道键盘是由哪个设备节点上报的,就可以通过以下这条指令来获取:
cat /proc/bus/input/devices
这条指令的含义就是获取与event对应的相关设备信息,在ubuntu系统上,我们输入这个指令可以看到以下结果:
那么这里的I、N、P、S、U、H、B对应的每一行是什么含义呢?
I:id of the device(设备ID)
该参数由结构体struct input_id来进行描述
41 struct input_id {
42 //总线类型
43 __u16 bustype;
44 //与厂商相关ID
45 __u16 vendor;
46 //与产品相关ID
47 __u16 product;
48 //版本ID
49 __u16 version;
50 };
N:name of the device
设备名称
P:physical path to the device in the system hierarchy
系统层次结构中设备的物理路径。
S:sysfs path
位于sys文件系统的路径
U:unique identification code for the device(if device has it)
设备的唯一标识码
H:list of input handles associated with the device.
与设备关联的输入句柄列表。
B:bitmaps(位图)
PROP:device properties and quirks.
EV:types of events supported by the device.
MSC:miscellaneous events supported by the device.
LED:leds present on the device.
PROP:设备属性和怪癖。
EV:设备支持的事件类型。
KEY:此设备具有的键/按钮。
MSC:设备支持的其他事件。
LED:设备上的指示灯。
通过了解以上参数的含义,结合以下指令
cat /proc/bus/input/devices
显示出来的信息很容易可以知道event1即是键盘上报的事件设备节点,通过读取这个event1即可获得当前用户按下的按键具体是哪个事件。
使用cat命令来测试键盘事件
当我们在终端输入
cat /dev/input/event1
这条指令并按回车键后可以看到一堆乱码数据,这些数据我们看不懂,但是我们可以知道如果按下了按键,终端有反馈消息,这时候就知道这个事件就是我们当前操作的这个设备上报的事件,那么如何能让这些数据看得懂呢?这时候可以使用hexdump命令来读取键盘事件。
使用hexdump命令来测试键盘事件
这些数值是通过input_event结构体来上报的,它位于/usr/include/linux/input.h这个头文件,input_event结构体描述如下:
24 struct input_event {
25 //事件发生的事件
26 struct timeval time;
27 //事件类型
28 __u16 type;
29 //事件值
30 __u16 code;
31 //该事件上报的数值
32 __s32 value;
33 };
而input_event结构体中的time即是:
1 struct timeval
2 {
3 __time_t tv_sec; /* Seconds. */
4 __suseconds_t tv_usec; /*Microseconds. */
5 };
其中tv_sec为Epoch到创建struct timeval时的秒数,tv_usec为微秒数,即秒后面的零头,Epoch的意思是指定为1970年一月一日凌晨零点零分零秒,格林威治时间。
回到input_event结构体,事件类型type主要有以下三种,分别是:相对事件、绝对事件、键盘事件
例如:鼠标就是一个相对事件,有些情况下也有可能是绝对事件,当移动鼠标的时候,type类型也就是底层上报给用户的事件类型,那么code表示的就是相对于鼠标当前的位置的X或者Y的坐标,value则表示相对于当前的位置偏移了多少。
事件类型(type)
文件头文件路径:
/usr/include/linux/input-event-codes.h
当然Linux内核版本较低的有可能在以下路径的这个头文件:
/usr/include/linux/input.h
34 /*
35 * Event types
36 */
37
38 #define EV_SYN 0x00 //同步事件
39 #define EV_KEY 0x01 //按键事件
40 #define EV_REL 0x02 //相对事件
41 #define EV_ABS 0x03 //绝对事件
42 #define EV_MSC 0x04
43 #define EV_SW 0x05
44 #define EV_LED 0x11
45 #define EV_SND 0x12
46 #define EV_REP 0x14
47 #define EV_FF 0x15
48 #define EV_PWR 0x16
49 #define EV_FF_STATUS 0x17
50 #define EV_MAX 0x1f
51 #define EV_CNT (EV_MAX+1)
事件值(code)
由于事件值种类繁多,这里就不一一列举出来,这里举例键盘的部分事件值:
文件头文件路径:
/usr/include/linux/input-event-codes.h
当然Linux内核版本较低的有可能在以下路径的这个头文件:
/usr/include/linux/input.h
64 /*
65 * Keys and buttons
66 *
67 * Most of the keys/buttons are modeled after USB HUT 1.12
68 * (see http://www.usb.org/developers/hidpage).
69 * Abbreviations in the comments:
70 * AC - Application Control
71 * AL - Application Launch Button
72 * SC - System Control
73 */
74
75 #define KEY_RESERVED 0
76 #define KEY_ESC 1
77 #define KEY_1 2
78 #define KEY_2 3
79 #define KEY_3 4
80 #define KEY_4 5
81 #define KEY_5 6
82 #define KEY_6 7
83 #define KEY_7 8
84 #define KEY_8 9
85 #define KEY_9 10
86 #define KEY_0 11
87 #define KEY_MINUS 12
88 #define KEY_EQUAL 13
89 #define KEY_BACKSPACE 14
90 #define KEY_TAB 15
91 #define KEY_Q 16
92 #define KEY_W 17
...
当然还有鼠标事件值、摇杆事件值、触摸屏事件值等等。
该事件上报的数值(value)
这部分上面已经举了鼠标的案例进行了介绍,接下来我们就通过应用程序来获取事件,后面章节将会通过鼠标、键盘以及触摸屏三个案例,进一步的了解输入系统的应用编程。
3.4 输入系统应用编程实战一:通用USB鼠标事件读取
根据前面章节的讲解,如果我们需要获取USB鼠标的事件,首先我们要先通过cat /proc/bus/input/devices这个指令查询与USB鼠标事件对应的相关设备信息,通过实际测试得知,event2为USB鼠标上报的事件节点。
接下来,通过hexdump命令测试一下鼠标事件的输出:
具体上报的数值是什么含义可以结合3.3章节进行分析,这里就不再进行阐述,本节的目的是编写一个获取通用USB鼠标的事件的应用程序,要获取一个事件,我们需要了解以下几个部分。
1 设备上报事件类型(type)
通过3.3章节,我们知道找到对应的事件类型的定义:
文件头文件路径:
/usr/include/linux/input-event-codes.h
当然Linux内核版本较低的有可能在以下路径的这个头文件:
/usr/include/linux/input.h
34 /*
35 * Event types
36 */
37
38 #define EV_SYN 0x00 //同步事件
39 #define EV_KEY 0x01 //按键事件
40 #define EV_REL 0x02 //相对事件
41 #define EV_ABS 0x03 //绝对事件
42 #define EV_MSC 0x04
43 #define EV_SW 0x05
44 #define EV_LED 0x11
45 #define EV_SND 0x12
46 #define EV_REP 0x14
47 #define EV_FF 0x15
48 #define EV_PWR 0x16
49 #define EV_FF_STATUS 0x17
50 #define EV_MAX 0x1f
51 #define EV_CNT (EV_MAX+1)
2 设备上报的事件值(code)
由于本节我们写的是通用USB鼠标的应用程序,所以我们找到鼠标相关的code,如下:
文件头文件路径:
/usr/include/linux/input-event-codes.h
当然Linux内核版本较低的有可能在以下路径的这个头文件:
/usr/include/linux/input.h
696 /*
697 * Relative axes
698 */
699
700 #define REL_X 0x00 //相对X坐标
701 #define REL_Y 0x01 //相对Y坐标
702 #define REL_Z 0x02
703 #define REL_RX 0x03
704 #define REL_RY 0x04
705 #define REL_RZ 0x05
706 #define REL_HWHEEL 0x06
707 #define REL_DIAL 0x07
708 #define REL_WHEEL 0x08
709 #define REL_MISC 0x09
710 #define REL_MAX 0x0f
711 #define REL_CNT (REL_MAX+1)
在这里,我们暂时只会用来REL_X和REL_Y这两个参数。
那么所谓的value,就是选择具体的事件类型(type)和具体的事件值(code)以后所反应出来的值,鼠标就是相对于当前X或者相对于当前Y的值,接下来,我们来看一下如何来读取鼠标事件。
在编写input应用程序之前,在程序中需要包含以下头文件:
#include <linux/input.h>
程序编写步骤:
1 定义一个结构体变量input_event用于描述input事件
struct input_event event_mouse ;
2 打开input设备的事件节点,这里我们获取的通用USB鼠标是event2
open("/dev/input/event2",O_RDONLY);
3 读取事件
read(fd ,&event_mouse ,sizeof(event_mouse));
4 根据上报的事件进行处理
//判断鼠标上报的类型,可能为绝对事件,也有可能是相对事件
if(EV_ABS == event_mouse.type || EV_REL == event_mouse.type)
{
//code表示相对位移X或者Y,当判断是X时,打印X的相对位移value
//当判断是Y时,打印Y的相对位移value
if(event_mouse.code == REL_X)
{
printf("event_mouse.code_X:%d\n", event_mouse.code);
printf("event_mouse.value_X:%d\n", event_mouse.value);
}
else if(event_mouse.code == REL_Y)
{
printf("event_mouse.code_Y:%d\n", event_mouse.code);
printf("event_mouse.value_Y:%d\n", event_mouse.value);
}
}
5 关闭文件描述符
close(fd);
不难发现,获取一个输入系统事件,也是标准的文件操作,这体现了Linux一切皆文件的思想。
完整的程序案例如下:
01 #include <stdio.h>
02 #include <unistd.h>
03 #include <stdlib.h>
04 #include <fcntl.h>
05 #include <linux/input.h>
06
07 int main(void)
08 {
09 //1、定义一个结构体变量用来描述input事件
10 struct input_event event_mouse ;
11 //2、打开input设备的事件节点 我的通用USB鼠标事件的节点是event2
12 int fd = -1 ;
13 fd = open("/dev/input/event2", O_RDONLY);
14 if(-1 == fd)
15 {
16 printf("open mouse event fair!\n");
17 return -1 ;
18 }
19 while(1)
20 {
21 //3、读事件
22 read(fd, &event_mouse, sizeof(event_mouse));
23 if(EV_ABS == event_mouse.type || EV_REL == event_mouse.type)
24 {
25 //code表示相对位移X或者Y,当判断是X时,打印X的相对位移value
26 //当判断是Y时,打印Y的相对位移value
27 if(event_mouse.code == REL_X)
28 {
29 printf("event_mouse.code_X:%d\n", event_mouse.code);
30 printf("event_mouse.value_X:%d\n", event_mouse.value);
31 }
32 else if(event_mouse.code == REL_Y)
33 {
34 printf("event_mouse.code_Y:%d\n", event_mouse.code);
35 printf("event_mouse.value_Y:%d\n", event_mouse.value);
36 }
37 }
38 }
39 close(fd);
40 return 0 ;
41 }
代码编写完毕后,然后执行
gcc test_mouse.c -o test_mouse
编译程序:
编译成功后会生成test_mouse,接下来执行test_mouse这个程序。
当鼠标左右移动的时候上报的事件:
这时候可以看到,只有相对于X的事件值在发生,这时候打印的value是X方向相对于原点坐标的偏移值。
当鼠标上下移动的时候上报的事件:
这时候可以看到,只有相对于Y的事件值在发生,这时候打印的value是Y方向相对于原点坐标的偏移值。
3.5 输入系统应用编程实战二:通用键盘事件读取
如何获取键盘事件在3.3章节已经有了相应的介绍,这里就不再写出来,本节实现的是通用键盘事件的获取,结合3.4章节获取鼠标事件的方式,这里通用键盘事件的节点为event1,通过结合3.3章节与3.4章节,编写步骤如下:
在编写input应用程序之前,在程序中需要包含以下头文件:
#include <linux/input.h>
程序编写步骤:
1 定义一个结构体变量input_event用于描述input事件
struct input_event event_keyboard ;
2 打开input设备的事件节点,我的通用键盘事件的节点是event1
open("/dev/input/event1",O_RDONLY);
3 读取事件
read(fd ,&event_keyboard ,sizeof(event_keyboard));
4 根据上报的事件进行处理
//判断键盘事件上报的类型
if(EV_KEY == event_keyboard.type)
{
if(1 == event_keyboard.value)
printf("事件类型:%d 事件值:%d 按下\n", event_keyboard.type, event_keyboard.code);
else if(0 == event_keyboard.value)
printf("事件类型:%d 事件值:%d 释放\n", event_keyboard.type, event_keyboard.code);
}
5 关闭文件描述符
close(fd);
完整程序案例实现如下:
01 #include <stdio.h>
02 #include <unistd.h>
03 #include <stdlib.h>
04 #include <fcntl.h>
05 #include <linux/input.h>
06
07 int main(void)
08 {
09 //1、定义一个结构体变量用来描述input事件
10 struct input_event event_keyboard ;
11 //2、打开input设备的事件节点 我的通用键盘事件的节点是event1
12 int fd = -1 ;
13 fd = open("/dev/input/event1", O_RDONLY);
14 if(-1 == fd)
15 {
16 printf("open mouse event fair!\n");
17 return -1 ;
18 }
19 while(1)
20 {
21 //3、读事件
22 read(fd, &event_keyboard, sizeof(event_keyboard));
23 if(EV_KEY == event_keyboard.type)
24 {
25 if(1 == event_keyboard.value)
26 printf("事件类型:%d 事件值:%d 按下\n",event_keyboard.type,event_keyboard.code);
27 else if(0 == event_keyboard.value)
28 printf("事件类型:%d 事件值:%d 释放\n",event_keyboard.type,event_keyboard.code);
29 }
30 }
31 close(fd);
32 return 0 ;
33 }
不难发现,通用USB键盘程序编写步骤与通用USB鼠标程序编写步骤几乎一样,区别只是读取的事件类型以及后面处理的数据value不同。
代码编写完毕后,然后执行
gcc test_keyboard.c -o test_keyboard
编译程序:
编译成功后会生成test_keyboard,接下来执行test_keyboard这个程序。
当按下按键时候,可以观察到按键的按下和释放的过程,这其实就是同一个事件下的两个不同的状态。
3.6 输入系统应用编程实战三:百问网imx6ul开发板触摸屏事件读取
在前面,我们已经熟悉了鼠标、键盘的基本操作,但发现一个规律,那就是编程方法类似,唯一不同的地方就是获取的事件类型以及事件值不同,那么触摸屏在input系统中是一类什么事件呢?
一般情况下,触摸屏在input系统中属于绝对事件,也就是触摸的坐标点X和Y会在屏幕的分辨率范围内上报一个绝对的坐标。
绝对事件对应的值为:EV_ABS
相应X和Y分量的值分别为:
ABS_MT_POSITION_X、ABS_MT_POSITION_Y
通过结合前面的章节内容,很容易编写如下程序:
01 #include <stdio.h>
02 #include <unistd.h>
03 #include <fcntl.h>
04 #include <stdlib.h>
05 #include <linux/input.h>
06
07 int main(int argc, char **argv)
08 {
09 int tp_fd = -1 ;
10 int tp_ret = -1 ;
11 int touch_x,touch_y ;
12 struct input_event imx6ull_ts ;
13 //1、打开触摸屏事件节点
14 tp_fd = open("/dev/input/event1",O_RDONLY);
15 if(tp_fd < 0)
16 {
17 printf("open /dev/input/event1 fail!\n");
18 return -1 ;
19 }
20 while(1)
21 {
22 //2、获取触摸屏相应的事件,并打印出当前触摸的坐标
23 read(tp_fd ,&imx6ull_ts ,sizeof(imx6ull_ts));
24 switch(imx6ull_ts.type)
25 {
26 case EV_ABS:
27 if(imx6ull_ts.code == ABS_MT_POSITION_X)
28 touch_x = imx6ull_ts.value ;
29 if(imx6ull_ts.code == ABS_MT_POSITION_Y)
30 touch_y = imx6ull_ts.value ;
31 break ;
32 defalut:
33 break ;
34 }
35 printf("touch_x:%d touch_y:%d\n",touch_x,touch_y);
36 usleep(100);
37 }
38 close(tp_fd);
39 return 0;
40 }
代码编写完毕后,然后执行
gcc test_touchscreen.c -o test_touchscreen
交叉编译程序:(注意这里是要在开发板运行,不是在PC端)
接下来启动开发板,然后串口终端输出rz命令,等待接收PC端的文件,这里我们将test_touchscreen这个文件传输到开发板。
具体操作步骤可参考第11章:PC和开发板之间传输文件
接下来给test_touchscreen添加可执行权限:
执行test_touchscreen,然后用手触摸屏,可以看到有相应的坐标值打印:
第四章:进程间通信
进程间通信
4 Linux进程间通信
4.1 初识进程
在日常工作/学习中,读者可能会经常听到如下一些词:“作业”,“任务”,“开了几个线程”,“创建了几个进程”,“多线程”,“多进程”等等。如果系统学习过《操作系统》这门课程,相信大家对这些概念都十分了解。但对很多电子、电气工程专业(或是其他非计算机专业)的同学来说,由于这门课程不是必修课程,我们脑海中可能就不会有这些概念,听到这些概念的时候就会不知所云,不过没有关系,先让我们克服对这些概念的恐惧。比如小时候刚开始学习数学的时候,先从正整数/自然数开始学习,然后逐步接触到分数、小数、负数、有理数、无理数、实数,再到复数等等。这些操作系统中的概念也是这样,让我们从初级阶段开始学起,逐步攻克这些新概念背后的真正含义。
本篇主要讨论linux进程间通信方式,这个主题拆分开始来看,分为三个部分:linux(操作系统)、进程、进程间通信。Linux操作系统本篇暂且不谈,我们主要来关注后两个部分:进程,以及进程间通信。在探讨进程间通信之前,让我们先关注一个知识点概念----进程。
4.1.1 进程的概念
4.1.1.1 程序
在探讨进程之前,先思考一个问题:什么是程序?
嵌入式软件工程师每天的工作/学习内容就是看C/C++源代码、分析C/C++源代码、编写C/C++源代码(有人会说,应该还有最重要的调试程序,我每天的工作日常是三分写程序,七分调试程序,调试程序去哪里了,大家别着急,这里先卖一个关子)。这些独立的源代码就是一个个程序。它们有一个共同特点,在我们阅读、分析、编写的过程中,此刻都是静态的,它们存储在我们的硬盘上、公司的服务器上。
程序:存储在磁盘上的指令和数据的有序集合。如下就是一个程序,此刻它正安静地躺在硬盘上。
01 #include <stdio.h>
02
03 int main(int argc, char *argv[])
04{
05 printf("hello world!\n");
06 return 0;
07}
4.1.1.2 进程
有了上面程序的概念,先直接给出进程的定义。
进程:具有一定独立功能的程序在一个数据集合上的一次动态执行过程。它是动态的,包括创建、调度、执行和消亡(由操作系统完成的)。
定义中的每个词分开来我们都能理解,但是组合到一起成为一个句子时,我们又不知道什么意思了。图灵奖得主Pascal之父尼古拉斯·沃斯,提出过一个著名的公式:程序=算法+数据结构。所谓算法就是解决一个问题的方法,程序就是使用算法对特定数据进行处理,这些数据是一个广义上的概念,不单单指像1,2,3,…等等这样的数据。因此用更直白的语言来说,程序开始运行,对数据进行分析处理的过程就是一个进程。
4.1.1.3 进程和程序的联系
-
程序是产生进程的基础。
-
程序的每次执行构成不同的进程。
-
进程是程序功能的体现(还记得之前提到的程序员日常工作中的一个重要事项----调试程序吗?调试的过程实际上就是程序的执行,就是本次程序功能的体现,因此这个时候它就是一个进程)。
-
通过多次执行,一个程序可对应多个进程;通过调用关系,一个进程可包含多个程序。
4.1.1.4 进程和程序的区别
程序 | 进程 | |
---|---|---|
状态 | 静态的,是有序代码的集合 | 动态的,是程序功能的执行过程 |
生命期 | 永久的,长久保存在存储设备上 | 暂时的,一个程序执行结束,则它对应的进程结束 |
下图反应了从程序到进程的变化过程。
我们以一个生活中的例子来加深对进程和程序的理解:
1.有一位计算机科学家,他的女儿要过生日了,他准备给女儿做一个生日蛋糕,于是他去找了一本菜谱,跟着菜谱学习做蛋糕。
菜谱=程序 科学家=CPU 做蛋糕的原材料=数据 做蛋糕的过程=进程
2.科学家正在做蛋糕的时候,突然他的小儿子跑过来,说他的手被扎破了,于是科学家又去找了一本医疗手册,给小儿子处理伤口,处理完伤口之后,继续做生日蛋糕
医疗手册=新程序 给小儿子处理伤口=新进程
从做蛋糕切换到优先包扎伤口=进程切换 处理完伤口继续做生日蛋糕=进程恢复
介绍到这里,希望读者对进程已经建立起一些基础概念了,有关进程的深入部分,我们在这里暂且先不介绍,比如进程的组成包括哪些(代码段,用户数据段,系统数据段)?进程的类型有哪些?进程的状态有哪些等等?这些深入内容,在我们掌握了进程的基础知识之后,读者有兴趣的话,可以查阅相关书籍资料。
4.1.2 进程的操作(创建、结束、回收)
4.1.2.1 创建进程
使用fork函数来创建一个进程
头文件: #include <unistd.h>
函数原型: pid_t fork(void);
返回值: 成功时,父进程返回子进程的进程号(>0的非零整数),子进程中返回0;通过fork函数的返回值区分父子进程。
父进程: 执行fork函数的进程。
子进程: 父进程调用fork函数之后,生成的新进程。
请重点注意:这个函数的返回值和我们接触的绝大部分函数的返回值不一样。
一般地,一个函数的返回值只有一个值,但是该函数的返回值却有两个。实际上关于这个函数的返回值究竟有几个,可以换一种方式来理解,因为这个函数执行之后,系统中会存在两个进程----父进程和子进程,在每个进程中都返回了一个值,所以给用户的感觉就是返回了两个值。
进程的特点:
-
在linux中,一个进程必须是另外一个进程的子进程,或者说一个进程必须有父进程,但是可以没有子进程。
-
子进程继承了父进程的内容,包括父进程的代码,变量,pcb,甚至包括当前PC值。在父进程中,PC值指向当前fork函数的下一条指令地址,因此子进程也是从fork函数的下一条指令开始执行。父子进程的执行顺序是不确定的,可能子进程先执行,也可能父进程先执行,取决于当前系统的调度。
-
父子进程有独立的地址空间、独立的代码空间,互不影响,就算父子进程有同名的全局变量,但是由于它们处在不同的地址空间,因此不能共享。
-
子进程结束之后,必须由它的父进程回收它的一切资源,否则就会成为僵尸进程。
-
如果父进程先结束,子进程会成为孤儿进程,它会被INIT进程收养,INIT进程是内核启动之后,首先被创建的进程。
Tips:
在linux下,当我们不熟悉某个系统接口API函数时(比如不知道调用这个函数需要包含的头文件,不知道这个函数的每个参数的意义等等),我们可以在ubuntu下使用man命令来查看这个函数的说明。
示例程序(参考:jz2440\process\1th_create_process\create_process.c)
01 /**********************************************************************
02 * 功能描述: 创建一个子进程
03 * 输入参数: 无
04 * 输出参数: 无
05 * 返 回 值: 无
06 * 修改日期 版本号 修改人 修改内容
07 * -----------------------------------------------
08 * 2020/05/16 V1.0 zh(ryan) 创建
09 ***********************************************************************/
10
11 #include <stdio.h>
12 #include <stdlib.h>
13 #include <unistd.h>
14 #include <sys/types.h>
15
16 int main(int argc, char *argv[])
17 {
18 pid_t pid;
19
20 pid = fork(); // 创建子进程
21
22 if (pid == 0) { // 子进程
23 int i = 0;
24 for (i = 0; i < 5; i++) {
25 usleep(100);
26 printf("this is child process i=%d\n", i);
27 }
28 }
29
30 if (pid > 0) { // 父进程
31 int i = 0;
32 for (i = 0; i < 5; i++) {
33 usleep(100);
34 printf("this is parent process i=%d\n", i);
35 }
36 }
37
38 while(1); //不让进程结束,以便我们查看进程的一些状态信息
39 return 0;
40 }
JZ2440实验
在jz2440开发板上实验,读者首先需要创建好NFS文件系统,jz2440开发板从网络文件系统启动,以便运行在ubuntu上编译好的可执行文件,关于如何搭建NFS文件系统请参考视频教程《u-boot_内核_根文件系统(ARM裸机1期加强版与2期驱动大全间的衔接)》。读者也可以在ubuntu上执行,将编译器从“arm-linux-gcc”换成“gcc”即可。
- 编译程序
arm-linux-gcc create_process.c -o create_process
- 将可执行文件test拷贝到NFS文件系统对应的目录下
cp create_process /work/nfs_root/first_fs
- 在jz2440开发板的串口下此时能看到该可执行文件
-
执行可执行文件
”&”表示在后台执行,这样我们可以继续在串口控制台下敲入命令,控制台能够接收到输入字符并作出响应;如果不加”&”,表示在前台执行,控制台不能对输入字符作出响应。
./create_process &
- top命令查看进程状态
top
发现此时确实存在两个进程create_process,其中一个进程PID是777(它的父进程PID是776),另外一个进程PID是776(它的父进程PID是770)。
4.1.2.2 结束进程
使用exit函数来结束一个进程
头文件: #include <stdlib.h>
函数原型: void exit (int status)
使用_exit函数来结束一个进程
头文件: #include <unistd.h>
函数原型: void _exit(int status);
两个函数的区别是:exit结束进程时会刷新缓冲区,_exit不会;
这两个退出函数和return函数又有什么区别呢?exit和 _ exit函数是返回给操作系统的,return函数是当前函数返回,返回到调用它的函数中,如果正好是在main函数中,return函数也返回给了操作系统,这个时候return和exit、_exit起到了类似的作用。
程序实验:验证exit和_exit的区别
示例1:使用exit退出(参考:jz2440\process\2th_exit_process\exit_process.c)
01 /**********************************************************************
02 * 功能描述: 使用exit退出当前进程
03 * 输入参数: 无
04 * 输出参数: 无
05 * 返 回 值: 无
06 * 修改日期 版本号 修改人 修改内容
07 * -----------------------------------------------
08 * 2020/05/16 V1.0 zh(ryan) 创建
09 ***********************************************************************/
10 #include <stdio.h>
11 #include <stdlib.h>
12
13 int main(int argc, char *argv[])
14 {
15 printf("hello world\n");
16 printf("will exit");
17 exit(0); //使用_exit退出
18 }
示例2:使用_exit退出(参考:jz2440\process\3th_exit_process\exit_process.c)
01 /**********************************************************************
02 * 功能描述: 使用_exit退出当前进程
03 * 输入参数: 无
04 * 输出参数: 无
05 * 返 回 值: 无
06 * 修改日期 版本号 修改人 修改内容
07 * -----------------------------------------------
08 * 2020/05/16 V1.0 zh(ryan) 创建
09 ***********************************************************************/
10 #include <stdio.h>
11 #include <stdlib.h>
12
13 int main(int argc, char *argv[])
14 {
15 printf("hello world\n");
16 printf("will exit");
17 _exit(0); //使用_exit退出
18 }
在两个示例程序中,第15行比第16行的打印语句多了一个“\n”,它会强制将待打印的字符刷新到缓冲区,为了对比exit和_exit的区别,在第16行中就没有加上“\n”,按照上面两个退出函数的区别,示例1应该会同时打印“hello world”和“will exit”,示例2程序只会打印“hello world”,不会打印“will exit”,那么到底是不是这样呢?我们在jz2440下验证一下。
JZ2440实验
示例1
- 编译
arm-linux-gcc exit_process.c -o exit_process
- 拷贝到NFS
cp exit_process /work/nfs_root/first_fs
- 运行
./exit_process
运行结果,确实同时打印了“hello world”和“will exit”
4.1.2.3 回收进程
使用wait函数来回收一个进程
头文件: #include <sys/types.h>
#include <sys/wait.h>
函数原型: pid_t wait(int *status);
返回值: 成功返回子进程的进程号,失败返回-1
使用waitpid函数来回收一个进程
头文件: #include <sys/types.h>
#include <sys/wait.h>
函数原型: pid_t waitpid(pid_t pid, int *status, int options);
返回值: 成功返回子进程的进程号,失败返回-1
程序示例:子进程退出,父进程回收子进程(参考:jz2440\process\4th_exit_wait\exit_wait.c)
1 /**********************************************************************
02 * 功能描述: 使用exit退出子进程,父进程使用waitpid回收子进程的资源
03 * 输入参数: 无
04 * 输出参数: 无
05 * 返 回 值: 无
06 * 修改日期 版本号 修改人 修改内容
07 * -----------------------------------------------
08 * 2020/05/16 V1.0 zh(ryan) 创建
09 ***********************************************************************/
10 #include <unistd.h>
11 #include <stdio.h>
12 #include <stdlib.h>
13 #include <sys/types.h>
14 #include <sys/wait.h>
15
16 int main(int argc, char *argv[])
17 {
18 int status = -1;
19 pid_t pid;
20
21 pid = fork();
22 if (pid == 0){ //子进程
23 printf("fork\n");
24 exit(1);
25 } else if (pid > 0) { //父进程
26 pid = waitpid(pid, &status, 0);
27 printf("status=0x%x\n", status);
28 } else {
29 perror("fork\n");
30 }
31
32 return 0;
33 }
JZ2440实验
- 编译
arm-linux-gcc exit_wait.c -o exit_wait
- 拷贝到NFS
cp exit_wait /work/nfs_root/first_fs
- 运行
./exit_wait
运行结果
4.2 进程为什么需要通信
先让我们看如下两个简单的程序,这两个程序中都有一个同名全局变量“global”,唯一的区别是这个全局变量的初始值不同。说明:以下两个示例程序是为了让我们理解进程的一个特点,因此实验环境是Ubuntu虚拟机。
程序1:
01 #include <stdio.h>
02 int global = 1;
03
04 void delay(void)
05 {
06 unsigned int a = 1000000;
07 while(a--);
08 }
09
10 int main(int argc, char *argv[])
11 {
12 while (1) {
13 printf("global=%d\n", global);
14 delay();
15 }
16 return 0;
17 }
程序2:
01 #include <stdio.h>
02 int global = 2;
03
04 void delay(void)
05 {
06 unsigned int a = 1000000;
07 while(a--);
08 }
09
10 int main(int argc, char *argv[])
11 {
12 while (1) {
13 printf("global=%d\n", global);
14 delay();
15 }
16 return 0;
17 }
两个程序的唯一区别如下红框所示:
- 编译程序
gcc test1.c -o test1
gcc test2.c -o test2
- 运行程序
./test1
./test2
程序1运行结果
程序2运行结果
我们发现,两个程序运行之后,当前进程中的全局变量global的值并不会改变,它不会被改变成另外一个进程中的值,由此引出的进程的一个特点:**进程资源的唯一性,不共享性,它不能访问别的进程中的数据(地址空间),也不能被别的进程访问本身的数据(地址空间)。**每个进程对其他进程而言,就是一个黑盒(后面读者学习到线程的时候,会发现在这个特性上,线程是有别于进程的)。
那么为什么会这样呢?这是因为操作系统为了保证系统的安全(进程A奔溃不会影响进程B,进程B仍然会继续运行),它会为每个进程分配特定的地址空间,每个进程只能在这个特定的地址空间执行指令、访问数据,如下图所示。程序需要访问某个变量时,都是通过变量地址去访问该变量的,在不同的进程中,同名变量对应不同的地址(处在当前进程地址空间范围内),进程无法访问分配给它的地址范围之外的地址空间,自然就无法获得其他进程中的变量值。
进程间为何需要通信呢?从上面的两个示例程序中,可以得知:不同进程之间无法互相访问对方的地址空间。但是在我们实际的项目开发中,为了实现各种各样的功能,不同进程之间一定需要数据交互,那么我们应该如何实现进程间数据交互呢?这就是进程间通信的目的:实现不同进程之间的数据交互。
在linux下,内存空间被划分为用户空间和内核空间,应用程序开发人员开发的应用程序都存在于用户空间,绝大部分进程都处在用户空间;驱动程序开发人员开发的驱动程序都存在于内核空间。
在用户空间,不同进程不能互相访问对方的资源,因此,在用户空间是无法实现进程间通信的。为了实现进程间通信,必须在内核空间,由内核提供相应的接口来实现,linux系统提供了如下四种进程通信方式。
进程间通信方式 | 分类 |
---|---|
管道通信 | 无名管道、有名管道 |
IPC通信 | 共享内存、消息队列、信号灯 |
信号通信 | 信号发送、接收、处理 |
socket通信 | 本地socket通信,远程socket通信 |
linux有一个最基本的思想----“一切皆文件”,内核中实现进程间通信也是基于文件读写思想。不同进程通过操作内核里的同一个内核对象来实现进程间通信,如下图所示,这个内核对象可以是管道、共享内存、消息队列、信号灯、信号,以及socket。
4.3 进程通信之管道通信
管道分为无名管道和有名管道,其特点如下
类型 | 特点 |
---|---|
无名管道 | 在文件系统中没有文件节点,只能用于具有亲缘关系的进程间通信(比如父子进程) |
有名管道 | 在文件系统中有文件节点,适用于在同一系统中的任意两个进程间通信 |
4.3.1 无名管道
4.3.1.1 特点
无名管道实际上就是一个单向队列,在一端进行读操作,在另一端进行写操作,所以需要两个文件描述符,描述符fd[0]指向读端,fd[1]指向写端。它是一个特殊的文件,所以无法使用简单open函数创建,我们需要pipe函数来创建。它只能用于具有亲缘关系的两个进程间通信。
4.3.1.2 创建无名管道
1.头文件#include <unistd.h>
2.函数原型: int pipe(int fd[2])
3.参数: 管道文件描述符,有两个文件描述符,分别是fd[0]和fd[1],管道有一个读端fd[0]和一个写端fd[1]
4.返回值: 0表示成功;1表示失败
4.3.1.3 读、写、关闭管道
1.读管道 read,读管道对应的文件描述符是fd[0]
2.写管道 write,写管道对应的文件描述符是fd[1]
3.关闭管道 close,因为创建管道时,会同时创建两个管道文件描述符,分别是读管道文件描述符fd[0]和写管道文件描述符fd[1],因此需要关闭两个文件描述符
4.3.1.4 无名管道实现进程间通信
程序示例1
(参考:jz2440\process_pipe\1th_write_pipe\my_pipe_write.c)
01 /**********************************************************************
02 * 功能描述: 创建一个管道,并向管道中写入字符串,然后从管道中读取,验证
03 能否读取之前写入的字符串
04 * 输入参数: 无
05 * 输出参数: 无
06 * 返 回 值: 无
07 * 修改日期 版本号 修改人 修改内容
08 * -----------------------------------------------
09 * 2020/05/16 V1.0 zh(ryan) 创建
10 ***********************************************************************/
11 #include <stdio.h>
12 #include <stdlib.h>
13 #include <unistd.h>
14
15 int main(int argc, char *argv[])
16 {
17 int fd[2];
18 int ret = 0;
19 char write_buf[] = "Hello linux";
20 char read_buf[128] = {0};
21
22 ret = pipe(fd);
23 if (ret < 0) {
24 printf("create pipe fail\n");
25 return -1;
26 }
27 printf("create pipe sucess fd[0]=%d fd[1]=%d\n", fd[0], fd[1]);
28
29 //向文件描述符fd[1]写管道
30 write(fd[1], write_buf, sizeof(write_buf));
31
32 //从文件描述符fd[0]读管道
33 read(fd[0], read_buf, sizeof(read_buf));
34 printf("read_buf=%s\n", read_buf);
35
36 close(fd[0]);
37 close(fd[1]);
38 return 0;
39 }
JZ2440实验
- 编译
arm-linux-gcc my_pipe_write.c -o my_pipe_write
- 拷贝到NFS文件系统
cp my_pipe_write /work/nfs_root/first_fs
- 运行
./my_pipe_write
运行结果,发现能够正确读到管道中的字符串” Hello linux”。
程序示例2
在利用无名管道实现进程间通信之前,先让我们看一下如下的程序:我们知道父子进程的执行顺序是不确定的,是受系统调度的。我们在父进程中创建一个子进程,我们想让父进程控制子进程的运行,父进程设置“process_inter=1”,当“process_inter=1”时,子进程才会执行打印操作,否则子进程不执行打印操作。我们看如下的程序能够实现我们的目的吗?
(参考:jz2440\process_pipe\2th_comm\test.c)
01 /**********************************************************************
02 * 功能描述: 1.在父进程中创建一个子进程,
03 2.父进程执行完后,将变量process_inter赋值为1;
04 3.子进程判断process_inter为1则执行后面的打印语句,否则不执行。
05 * 输入参数: 无
06 * 输出参数: 无
07 * 返 回 值: 无
08 * 修改日期 版本号 修改人 修改内容
09 * -----------------------------------------------
10 * 2020/05/16 V1.0 zh(ryan) 创建
11 ***********************************************************************/
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include <unistd.h>
15 #include <sys/types.h>
16
17 int main(int argc, char *argv[])
18 {
19 pid_t pid;
20 int process_inter = 0;
21
22 pid = fork(); // 创建子进程
23
24 if (pid == 0) { // 子进程
25 int i = 0;
26 while (process_inter == 0); //
27 for (i = 0; i < 5; i++) {
28 usleep(100);
29 printf("this is child process i=%d\n", i);
30 }
31 }
32
33 if (pid > 0) { // 父进程
34 int i = 0;
35 for (i = 0; i < 5; i++) {
36 usleep(100);
37 printf("this is parent process i=%d\n", i);
38 }
39 process_inter == 1;
40 }
41
42 while(1);
43 return 0;
44 }
JZ2440实验
- 编译
arm-linux-gcc test.c -o test
- 拷贝到NFS文件系统
cp test /work/nfs_root/first_fs
- 运行
./test
运行结果,发现第29行打印语句一直没有,子进程中process_inter一直为0。
程序示例3
(参考:jz2440\process_pipe\3th_pipe_comm\comm_fork.c)
01 /**********************************************************************
02 * 功能描述: 1.使用无名管道实现父子进程通信
03 2.父进程向管道中写入一个值
04 3.子进程从管道中读取该值,如果非零,则执行后面的打印,否则不执行
05 * 输入参数: 无
06 * 输出参数: 无
07 * 返 回 值: 无
08 * 修改日期 版本号 修改人 修改内容
09 * -----------------------------------------------
10 * 2020/05/16 V1.0 zh(ryan) 创建
11 ***********************************************************************/
12
13 #include <stdio.h>
14 #include <stdlib.h>
15 #include <unistd.h>
16 #include <sys/types.h>
17
18 int main(int argc, char *argv[])
19 {
20 pid_t pid;
21 char process_inter = 0;
22 int fd[2], ret = 0;
23
24 ret = pipe(fd); //创建一个无名管道,必须在创建子进程之前
25 if (ret < 0) {
26 printf("create pipe fail\n");
27 return -1;
28 }
29 printf("create pipe sucess\n");
30
31 pid = fork(); //创建子进程
32
33 if (pid == 0) { // 子进程
34 int i = 0;
35 read(fd[0], &process_inter, sizeof(process_inter)); // 如果管道为空,则休眠等待
36 while (process_inter == 0);
37 for (i = 0; i < 5; i++) {
38 usleep(100);
39 printf("this is child process i=%d\n", i);
40 }
41 } else if (pid > 0) { // 父进程
42 int i = 0;
43 for (i = 0; i < 5; i++) {
44 usleep(100);
45 printf("this is parent process i=%d\n", i);
46 }
47 process_inter = 1;
48 sleep(2);
49 write(fd[1], &process_inter, sizeof(process_inter));
50 }
51
52 while(1);
53 return 0;
54 }
JZ2440实验
- 编译
arm-linux-gcc comm_fork.c -o comm_fork
- 拷贝到NFS文件系统
cp comm_fork /work/nfs_root/first_fs
- 运行
./comm_fork
运行结果,因为第38行2s延时,父进程打印结束后大约2s左右的时间,子进程中的打印也正确输出了,如下所示。
4.3.2 有名管道
4.3.2.1 特点
所谓有名管道,顾名思义,就是在内核中存在一个文件名,表明这是一个管道文件。Linux中存在7种文件类型,分别如下。
文件类型 | 文件特点 |
---|---|
普通文件 | 标识符 ‘-’ ,用open方式创建 |
目录文件 | 标识符 ‘d’ ,用mkdir方式创建 |
链接文件 | 标识符 ‘l’, la -s, 又可以分为软链接,硬链接 |
(有名)管道文件 | 标识 ‘p’, 用mkfifo创建 |
socket文件 | 标识符 ‘s’,用socket创建 |
字符设备文件 | 标识符 ‘c’ |
块设备文件 | 标识符 ‘b’ |
有名管道既可以用于具有亲缘关系的进程间通信,又可以用于非亲缘关系的进程间通信,在我们的实际项目中,很多进程之间是不具有亲缘关系的,因此有名管道使用的情况会更普遍一些。
4.3.2.2 创建有名管道
函数原型 : int mkfifo(const char * filename, mode_t mode)
参数 :管道文件文件名,权限,创建的文件权限仍然和umask有关系
返回值 : 成功返回0,失败返回-1
注意:mkfifo并没有在内核中生成一个管道,只是在用户空间生成了一个有名管道文件
4.3.2.3 有名管道实现进程间通信
示例程序1
创建一个有名管道文件(参考:jz2440\process_pipe\4th_create_myfifo\create_myfifo.c)
01 /**********************************************************************
02 * 功能描述: 1.创建一个有名管道
03 * 输入参数: 无
04 * 输出参数: 无
05 * 返 回 值: 无
06 * 修改日期 版本号 修改人 修改内容
07 * -----------------------------------------------
08 * 2020/05/16 V1.0 zh(ryan) 创建
09 ***********************************************************************/
10
11 #include <stdio.h>
12 #include <stdlib.h>
13 #include <unistd.h>
14 #include <sys/types.h>
15
16 int main(int argc, char *argv[])
17 {
18 int ret;
19
20 ret = mkfifo("./myfifo", 0777); //创建有名管道,文件权限为777
21 if (ret < 0) {
22 printf("create myfifo fail\n");
23 return -1;
24 }
25 printf("create myfifo sucess\n");
26
27 return 0;
28 }
JZ2440实验
- 编译
arm-linux-gcc create_myfifo.c -o create_myfifo
- 拷贝到NFS文件系统
cp create_myfifo /work/nfs_root/first_fs
- 运行
./create_myfifo
运行结果,发现在当前目录下生成一个有名管道文件myfifo(文件类型是“-p”)。
示例程序2
进程1源码(参考:jz2440\process_pipe\5th_myfifo_comm\5nd_named_pipe.c)
01 /**********************************************************************
02 * 功能描述: 1.进程1中创建一个有名管道3rd_fifo,权限是0777
03 2.以写方式打开这个有名管道文件,并向其中写入一个值
04 * 输入参数: 无
05 * 输出参数: 无
06 * 返 回 值: 无
07 * 修改日期 版本号 修改人 修改内容
08 * -----------------------------------------------
09 * 2020/05/16 V1.0 zh(ryan) 创建
10 ***********************************************************************/
11
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include <unistd.h>
15 #include <sys/types.h>
16 #include <fcntl.h>
17
18 int main(int argc, char *argv[])
19 {
20 int i, ret, fd;
21 char p_flag = 0;
22
23 /* 创建有名管道 */
24 if (access("./3rd_fifo", 0) < 0) { //先判断有名管道文件是否存在,不存在需要先创建
25 ret = mkfifo("./3rd_fifo", 0777);
26 if (ret < 0) {
27 printf("create named pipe fail\n");
28 return -1;
29 }
30 printf("create named pipe sucess\n");
31 }
32
33 /* 打开有名管道,以写方式打开 */
34 fd=open("./3rd_fifo", O_WRONLY);
35 if (fd < 0) {
36 printf("open 3rd_fifo fail\n");
37 return -1;
38 }
39 printf("open 3rd_fifo sucess\n");
40
41 for (i = 0; i < 5; i++) {
42 printf("this is first process i=%d\n", i);
43 usleep(100);
44 }
45 p_flag = 1;
46 sleep(5);
47 write(fd, &p_flag, sizeof(p_flag));
48
49 while(1);
50 return 0;
51 }
进程2源码(参考:jz2440\process_pipe\5th_myfifo_comm\5nd_named_pipe_2.c)
01 /**********************************************************************
02 * 功能描述: 1.只读方式打开这个有名管道文件,并读取这个值
03 2.当这个值非零时,继续执行后面的打印输出语句
04 * 输入参数: 无
05 * 输出参数: 无
06 * 返 回 值: 无
07 * 修改日期 版本号 修改人 修改内容
08 * -----------------------------------------------
09 * 2020/05/16 V1.0 zh(ryan) 创建
10 ***********************************************************************/
11
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include <unistd.h>
15 #include <sys/types.h>
16 #include <fcntl.h>
17
18 int main(int argc, char *argv[])
19 {
20 int i;
21 int fd=open("./3rd_fifo", O_RDONLY);
22 char p_flag = 0;
23
24 if (fd < 0) {
25 printf("open 3rd_fifo fail\n");
26 return -1;
27 }
28
29 printf("open 3rd_fifo sucess\n");
30 read(fd, &p_flag, sizeof(p_flag));
31 while(!p_flag);
32 for (i = 0; i < 5; i++) {
33 printf("this is second process i=%d\n", i);
34 usleep(100);
35 }
36
37 while(1);
38 return 0;
39 }
JZ2440实验
- 编译
arm-linux-gcc 5nd_named_pipe.c -o 5nd_named_pipe
arm-linux-gcc 5nd_named_pipe_2.c -o 5nd_named_pipe_2
- 拷贝到NFS文件系统
cp 5nd_named_pipe /work/nfs_root/first_fs
cp 5nd_named_pipe_2 /work/nfs_root/first_fs
- 运行
注意我们这里都在后台运行可执行程序,方便我们在串口中断下多次输入。
./5nd_named_pipe &
./5nd_named_pipe_2 &
4.4 进程通信之IPC通信
IPC通信分为共享内存、消息队列以及信号灯。这些IPC对象(共享内存、消息队列、信号灯)都存在于内核空间中。
应用程序使用IPC通信的一般步骤如下:
-
首先生成一个key值。有两种生成key的方式,一种是使用宏IPC_PRIVATE表示一个key,它表示一个私有对象,只能用于当前进程或者具有亲缘关系的进程访问。另一种是使用ftok函数来生成一个key值,这种方式创建的IPC对象可以被不同的进程访问。
-
使用生成的key值,创建一个IPC对象(如果是已经创建好的IPC对象,则打开该IPC对象),这个时候每个IPC对象都有一个唯一的ID号(IPC_id,可以是shm_id,msg_id,sem_id,每个id代表一个IPC对象)。
-
进程通过IPC_id,调用访问IPC通道的读写函数来操作IPC对象。调用shmctrl,shmat,shmdt来访问共享内存;调用msgctrl,msgsnd,msgrecv访问消息队列;调用semctrl,semop访问信号灯。
如何理解key和IPC_id(shm_id/msg_id/sem_id)
回答这个问题,请先思考一个问题,应用程序如何访问一个IPC对象(共享内存,消息队列、信号量灯)?
显然,我们需要一个唯一表示该IPC对象的身份ID(IPC_id,该IPC_id是由操作系统来管理的),但是由于这个ID只在当前创建该IPC对象的进程中可以获取到,在别的IPC进程中都没法获取,那么如何得到IPC对象的ID呢?这个时候就需要key值了,它相当于IPC_id的一个别名,或者叫做外部名,因此key值必须也是唯一的,这样才能得到唯一的IPC对象id。不同进程通过同一个key值得到同一个IPC对象id,来访问同一个IPC对象。如下图所示
ftok函数
函数原型 : char ftok(const char *path, char key)
参数 : path,存在并且可以访问的文件路径
key,一个字符
返回值 : 正确返回一个key值,出错返回-1
为何需要ftok函数先生成key,然后再创建IPC对象?
这就类似于无名管道和有名管道的区别,使用IPC_PRIVATE宏创建的共享内存就类似于无名管道,只能实现有亲缘关系的进程间通信。
那么为什么又需要使用ftok生成一个key值呢?是否可以直接指定一个非零值呢?直接指定一个非零的key值做法是不建议的,因为读者自己指定的key值很有可能于系统中已经存在的key值一样。
ftok函数创建了一个key值之后,就类似于有名管道,既可以实现具有亲缘关系的进程间通信,又能够实现非亲缘关系的进程间通信。
4.4.1 共享内存
4.4.1.1 特点
所谓共享内存是指多个进程都可以访问的同一块地址空间,但是我们知道Linux操作系统为了保证系统执行的安全,为每个进程划分了各自独立的地址空间,每个进程不能访问别的进程的地址空间,那么共享内存实现的原理是什么呢?
内核开辟一块物理内存区域,进程本身将这片内存空间映射到自己的地址空间进行读写。
从图中可以看到,进程可以直接访问这片内存,数据不需要在两进程间复制,所以速度较快。共享内存没有任何的同步与互斥机制,所以要使用信号量来实现对共享内存的存取的同步。
当需要使用共享内存进行通信时,一般步骤如下:
-
先创建一片共享内存,该内存存在于内核空间中。
-
进程通过key值找到这片共享内存的唯一ID,然后将这片共享内存映射到自己的地址空间。
-
每个进程通过读写映射后的地址,来访问内核中的共享内存。
4.4.1.2创建共享内存
函数原型 : int shmget(key_t key, int size, int shmflg)
头文件: #include <sys/shm.h>
函数参数 : key: IPC_PRIVATE 或 ftok的返回值
IPC_PRIVATE返回的key值都是一样的,都是0
size : 共享内存区大小
shmflg : 同open函数的权限位,也可以用八进制表示法
返回值 : 成功,共享内存段标识符ID; -1 出错
程序示例1(参考jz2440\process_ipc\1st_shm\1st_shm.c)
01 /**********************************************************************
02 * 功能描述: 1.使用IPC_PRIVATE创建共享内存
03 * 输入参数: 无
04 * 输出参数: 无
05 * 返 回 值: 无
06 * 修改日期 版本号 修改人 修改内容
07 * -----------------------------------------------
08 * 2020/05/16 V1.0 zh(ryan) 创建
09 ***********************************************************************/
10
11 #include <stdio.h>
12 #include <stdlib.h>
13 #include <unistd.h>
14 #include <sys/types.h>
15 #include <sys/shm.h>
16 #include <signal.h>
17
18 int main(int argc, char *argv[])
19 {
20 int shmid;
21
22 shmid = shmget(IPC_PRIVATE, 128, 0777);
23 if (shmid < 0) {
24 printf("create shared memory fail\n");
25 return -1;
26 }
27 printf("create shared memory sucess, shmid = %d\n", shmid);
28 system("ipcs -m");
29 return 0;
30 }
JZ2440实验
- 编译
arm-linux-gcc 1st_shm.c -o 1st_shm
- 拷贝到NFS文件系统
cp 1st_shm /work/nfs_root/first_fs
- 运行
执行第18行程序后,会在串口打印如下信息,这行语句的作用和我们直接在串口console下面输入“ipcs -m”是一样的。我们发现此时共享内存的key值为0。
./1st_shm
程序示例2(参考jz2440\process_ipc\1st_shm\2nd_shm.c)
程序源码,使用fotk函数生成一个key值
01 /**********************************************************************
02 * 功能描述: 1.使用ftok函数生成的key创建共享内存
03 * 输入参数: 无
04 * 输出参数: 无
05 * 返 回 值: 无
06 * 修改日期 版本号 修改人 修改内容
07 * -----------------------------------------------
08 * 2020/05/16 V1.0 zh(ryan) 创建
09 ***********************************************************************/
10
11 #include <stdio.h>
12 #include <stdlib.h>
13 #include <unistd.h>
14 #include <sys/types.h>
15 #include <sys/shm.h>
16 #include <signal.h>
17
18 int main(int argc, char *argv[])
19 {
20 int shmid;
21 int key;
22
23 key = ftok("./a.c", 'a'); //先创建一个key值
24 if (key < 0) {
25 printf("create key fail\n");
26 return -1;
27 }
28 printf("create key sucess key = 0x%X\n",key);
29
30 shmid = shmget(key, 128, IPC_CREAT | 0777);
31 if (shmid < 0) {
32 printf("create shared memory fail\n");
33 return -1;
34 }
35 printf("create shared memory sucess, shmid = %d\n", shmid);
36 system("ipcs -m");
37 return 0;
38 }
JZ2440实验
- 编译
arm-linux-gcc 2nd_shm.c -o 2nd_shm
- 拷贝到NFS文件系统
cp 2nd_shm /work/nfs_root/first_fs
- 运行
我们需要在2nd_shm所在的同级目录下创建一个文件a.c(在jz2440开发板上)
touch a.c
我们发现此时共享内存的key值为非零值0x610d0169.
./2nd_shm
4.4.1.3 应用程序如何访问共享内存
我们知道创建的共享内存还是处于内核空间中,用户程序不能直接访问内核地址空间,那么用户程序如何访问这个共享内存呢?
shmat函数
将共享内存映射到用户空间,这样应用程序就可以直接访问共享内存了
函数原型 : void *shmat(int shmid, const void *shmaddr, int shmflg)
参数 : shmid ID号
shmaddr 映射地址, NULL为系统自动完成的映射
shmflg SHM_RDONLY共享内存只读
默认是0,可读可写
返回值:成功,映射后的地址;失败,返回NULL
程序示例(参考jz2440\process_ipc\1st_shm\3nd_shm.c)
01 /**********************************************************************
02 * 功能描述: 1.创建共享内存,将该共享内存地址通过shmat映射到用户地址空间
03 2.用户通过标准输入向这个共享内存中输入一行字符串
04 3.然后从该共享内存中读取内容,验证是否能够读取到
05 * 输入参数: 无
06 * 输出参数: 无
07 * 返 回 值: 无
08 * 修改日期 版本号 修改人 修改内容
09 * -----------------------------------------------
10 * 2020/05/16 V1.0 zh(ryan) 创建
11 ***********************************************************************/
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include <unistd.h>
15 #include <sys/types.h>
16 #include <sys/shm.h>
17 #include <signal.h>
18
19 int main(int argc, char *argv[])
20 {
21 int shmid;
22 int key;
23 char *p;
24
25 key = ftok("./a.c", 'b');
26 if (key < 0) {
27 printf("create key fail\n");
28 return -1;
29 }
30 printf("create key sucess key = 0x%X\n",key);
31
32 shmid = shmget(key, 128, IPC_CREAT | 0777);
33 if (shmid < 0) {
34 printf("create shared memory fail\n");
35 return -1;
36 }
37 printf("create shared memory sucess, shmid = %d\n", shmid);
38 system("ipcs -m");
39
40 p = (char *)shmat(shmid, NULL, 0);
41 if (p == NULL) {
42 printf("shmat fail\n");
43 return -1;
44 }
45 printf("shmat sucess\n");
46
47 //等待console输入,然后向共享内存写入数据
48 fgets(p, 128, stdin);
49
50 //读共享内存
51 printf("share memory data:%s\n", p);
52
53 //再次读共享内存
54 printf("share memory data:%s\n", p);
55 return 0;
56 }
JZ2440实验
- 编译
arm-linux-gcc 3nd_shm.c -o 3nd_shm
- 拷贝到NFS文件系统
cp 3nd_shm /work/nfs_root/first_fs
- 运行
我们需要在3nd_shm所在的同级目录下创建一个文件a.c(在jz2440开发板上)
touch a.c
此时会提示用户输入信息
./3nd_shm
我们在console下输入任意字符,比如“hello linux”,然后按下回车,发现打印如下
问题:代码中第51行读了一遍共享内存,然后第54行又读了一遍共享内存,发现两次都能读到共享内存的内容,说明共享内存被读了之后,内容仍然存在。而在管道中,读了一遍管道内容之后,如果紧接着读取第二遍(在没有新写入的前提下),我们是不能读到管道中的内容的,说明管道只要读取一次之后,内容就消失了,读者可以通过实验自行验证一下。
shmdt函数
函数原型:int shmdt(const void *shmaddr)
参数 ; shmat的返回值
返回值 : 成功0,出错-1
程序示例(参考jz2440\process_ipc\1st_shm\4th_shm.c)
01 /**********************************************************************
02 * 功能描述: 1.创建共享内存,将该共享内存地址通过shmat映射到用户地址空间
03 2.用户通过标准输入向这个共享内存中输入一行字符串
04 3.然后从该共享内存中读取内容
05 4.调用shmdt解除地址映射,此时应用程序继续访问会出错
06 * 输入参数: 无
07 * 输出参数: 无
08 * 返 回 值: 无
09 * 修改日期 版本号 修改人 修改内容
10 * -----------------------------------------------
11 * 2020/05/16 V1.0 zh(ryan) 创建
12 ***********************************************************************/
13 #include <stdio.h>
14 #include <stdlib.h>
15 #include <unistd.h>
16 #include <sys/types.h>
17 #include <sys/shm.h>
18 #include <signal.h>
19 #include <string.h>
20
21 int main(int argc, char *argv[])
22 {
23 int shmid;
24 int key;
25 char *p;
26
27 key = ftok("./a.c", 'b');
28 if (key < 0) {
29 printf("create key fail\n");
30 return -1;
31 }
32 printf("create key sucess key = 0x%X\n",key);
33
34 shmid = shmget(key, 128, IPC_CREAT | 0777);
35 if (shmid < 0) {
36 printf("create shared memory fail\n");
37 return -1;
38 }
39 printf("create shared memory sucess, shmid = %d\n", shmid);
40 system("ipcs -m");
41
42 p = (char *)shmat(shmid, NULL, 0);
43 if (p == NULL) {
44 printf("shmat fail\n");
45 return -1;
46 }
47 printf("shmat sucess\n");
48
49 //write share memory
50 fgets(p, 128, stdin);
51
52 //start read share memory
53 printf("share memory data:%s\n", p);
54
55 //start read share memory again
56 printf("share memory data:%s\n", p);
57
58 //在用户空间删除共享内存的地址
59 shmdt(p);
60
61 memcpy(p, "abcd", 4); //执行这个语句会出现segment fault,因为解除了共享内存地址映射
62 return 0;
63 }
JZ2440实验
- 编译
arm-linux-gcc 4th_shm.c -o 4th_shm
- 拷贝到NFS文件系统
cp 4th_shm /work/nfs_root/first_fs
- 运行
我们需要在4th_shm.c所在的同级目录下创建一个文件a.c(在jz2440开发板上)
touch a.c
运行,此时会提示用户输入信息,输入完之后,执行第61行语句会出现Segmentation fault,这是程序期待的现象。
./4th_shm
shmctl函数
函数原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf)
参数: shmid : 共享内存标识符
cmd : IPC_START (获取对象属性) --- 实现了命令 ipcs -m
IPC_SET(设置对象属性)
IPC_RMID (删除对象属性) --- 实现了命令 ipcrm -m
buf : 指定IPC_START/IPC_SET时用以保存/设置属性
返回值 : 成功0,出错-1
程序示例(参考jz2440\process_ipc\1st_shm\5th_shm.c)
01 /**********************************************************************
02 * 功能描述: 1.创建共享内存,将该共享内存地址通过shmat映射到用户地址空间
03 2.用户通过标准输入向这个共享内存中输入一行字符串
04 3.然后从该共享内存中读取内容
05 4.调用shmdt解除地址映射,此时应用程序继续访问会出错
06 5.最后调用shmctl函数删除内核中的共享内存
07 * 输入参数: 无
08 * 输出参数: 无
09 * 返 回 值: 无
10 * 修改日期 版本号 修改人 修改内容
11 * -----------------------------------------------
12 * 2020/05/16 V1.0 zh(ryan) 创建
13 ***********************************************************************/
14
15 #include <stdio.h>
16 #include <stdlib.h>
17 #include <unistd.h>
18 #include <sys/types.h>
19 #include <sys/shm.h>
20 #include <signal.h>
21 #include <string.h>
22
23 int main(int argc, char *argv[])
24 {
25 int shmid;
26 int key;
27 char *p;
28
29 key = ftok("./a.c", 'b');
30 if (key < 0) {
31 printf("create key fail\n");
32 return -1;
33 }
34 printf("create key sucess key = 0x%X\n",key);
35
36 shmid = shmget(key, 128, IPC_CREAT | 0777);
37 if (shmid < 0) {
38 printf("create shared memory fail\n");
39 return -1;
40 }
41 printf("create shared memory sucess, shmid = %d\n", shmid);
42 system("ipcs -m");
43
44 p = (char *)shmat(shmid, NULL, 0);
45 if (p == NULL) {
46 printf("shmat fail\n");
47 return -1;
48 }
49 printf("shmat sucess\n");
50
51 //write share memory
52 fgets(p, 128, stdin);
53
54 //start read share memory
55 printf("share memory data:%s\n", p);
56
57 //start read share memory again
58 printf("share memory data:%s\n", p);
59
60 //在用户空间删除共享内存的地址
61 shmdt(p);
62
63 //memcpy(p, "abcd", 4); //执行这个语句会出现segment fault
64
65 shmctl(shmid, IPC_RMID, NULL);
66 system("ipcs -m");
67 return 0;
68 }
JZ2440实验
- 编译
arm-linux-gcc 5th_shm.c -o 5th_shm
- 拷贝到NFS文件系统
cp 5th_shm /work/nfs_root/first_fs
- 运行
touch a.c
运行。此时会提示用户输入信息,第一次执行第42行语句时,读者可以看到共享内存,第二次执行第66行语句时,读者就看不到共享内存了,因为此时共享内存已经被删除了。
./4th_shm
4.4.1.4 共享内存实现进程间通信
步骤:
1. 创建/打开共享内存
2. 映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问
3. 读写共享内存
4. 撤销共享内存映射
5. 删除共享内存对象
使用共享内存时的一些注意点或是限制条件
1. 共享内存的数量是有限制的,通过ipcs -l命令查看,当然如果我们具有管理员权限,可以通过 cat /proc/sys/kernel/shmmax来查看
2. 共享内存删除的时间点,shmctl添加删除标记,只有当所有进程都取消共享内存映射时(即所有进程调用shmdt之后),才会删除共享内存。
示例源码(参考jz2440\process_ipc\1st_shm\6th_shm.c)
01 /**********************************************************************
02 * 功能描述: 1.在父进程中创建使用key值为IPC_PRIVATE创建一个共享内存
03 2.然后在父进程中创建一个子进程
04 3.通过标准输入,父进程向共享内存中写入字符串
05 4.父进程调用发送信号函数通知子进程可以读取共享内存了
06 5.子进程收到父进程发送过来的信号,开始读取共享内存
07 6.子进程读完共享内存后,发送信号通知父进程读取完成
08 * 输入参数: 无
09 * 输出参数: 无
10 * 返 回 值: 无
11 * 修改日期 版本号 修改人 修改内容
12 * -----------------------------------------------
13 * 2020/05/16 V1.0 zh(ryan) 创建
14 ***********************************************************************/
15
16 #include <stdio.h>
17 #include <stdlib.h>
18 #include <unistd.h>
19 #include <sys/types.h>
20 #include <sys/shm.h>
21 #include <signal.h>
22 #include <string.h>
23
24 void myfun(int signum)
25 {
26 return;
27 }
28
29 int main(int argc, char *argv[])
30 {
31 int shmid;
32 int key;
33 char *p;
34 int pid;
35
36
37 shmid = shmget(IPC_PRIVATE, 128, IPC_CREAT | 0777);
38 if (shmid < 0) {
39 printf("create shared memory fail\n");
40 return -1;
41 }
42 printf("create shared memory sucess, shmid = %d\n", shmid);
43
44 pid = fork();
45 if (pid > 0) { // 父进程
46 signal(SIGUSR2, myfun);
47 p = (char *)shmat(shmid, NULL, 0);
48 if (p == NULL) {
49 printf("shmat fail\n");
50 return -1;
51 }
52 printf("parent process shmat sucess\n");
53 while (1) {
54 //从标准输入获取字符串,将其写入到共享内存
55 printf("parent process begin to write memory data:");
56 fgets(p, 128, stdin);
57 kill(pid, SIGUSR1); // 发信号通知子进程读共享内存
58 pause(); // 等待子进程读完共享内存的信号
59 }
60 }
61 if (pid == 0) { // 子进程
62 signal(SIGUSR1, myfun);
63 p = (char *)shmat(shmid, NULL, 0);
64 if (p == NULL) {
65 printf("shmat fail\n");
66 return -1;
67 }
68 printf("child process shmat sucess\n");
69 while (1) {
70 pause(); // 等待父进程发信号,准备读取共享内存
71 //子进程开始读共享内存,并发信号给父进程告知读完成
72 printf("child process read share memory data:%s\n", p);
73 kill(getppid(), SIGUSR2);
74 }
75 }
76
77 //在用户空间删除共享内存的地址
78 shmdt(p);
79
80 //memcpy(p, "abcd", 4); //执行这个语句会出现segment fault
81
82 shmctl(shmid, IPC_RMID, NULL);
83 system("ipcs -m");
84 return 0;
85 }
JZ2440实验
- 编译
arm-linux-gcc 6th_shm.c -o 6th_shm
- 拷贝到NFS文件系统
cp 6th_shm /work/nfs_root/first_fs
- 运行
父进程从标准输入获取用户输入的字符串,然后子进程会打印出该字符串。
./6th_shm
server进程源码(参考jz2440\process_ipc\1st_shm\7th_shm_1.c)
01 /**********************************************************************
02 * 功能描述: 1.server进程使用ftok生成一个key值,利用这个key值创建一个共享内存
03 2.通过标准输入,向共享内存中写入字符串
04 3.server进程调用发送信号函数通知client进程
05 * 输入参数: 无
06 * 输出参数: 无
07 * 返 回 值: 无
08 * 修改日期 版本号 修改人 修改内容
09 * -----------------------------------------------
10 * 2020/05/16 V1.0 zh(ryan) 创建
11 ***********************************************************************/
12
13 #include <stdio.h>
14 #include <stdlib.h>
15 #include <unistd.h>
16 #include <sys/types.h>
17 #include <sys/shm.h>
18 #include <signal.h>
19 #include <string.h>
20
21 struct mybuf
22 {
23 int pid;
24 char buf[124];
25 };
26
27 void myfun(int signum)
28 {
29 return;
30 }
31
32 int main(int argc, char *argv[])
33 {
34 int shmid;
35 int key;
36 struct mybuf *p;
37 int pid;
38
39 key = ftok("./a.c", 'a');
40 if (key < 0) {
41 printf("create key fail\n");
42 return -1;
43 }
44 printf("create key sucess\n");
45
46 shmid = shmget(key, 128, IPC_CREAT | 0777);
47 if (shmid < 0) {
48 printf("create shared memory fail\n");
49 return -1;
50 }
51 printf("create shared memory sucess, shmid = %d\n", shmid);
52
53 signal(SIGUSR2, myfun);
54 p = (struct mybuf *)shmat(shmid, NULL, 0);
55 if (p == NULL) {
56 printf("shmat fail\n");
57 return -1;
58 }
59 printf("parent process shmat sucess\n");
60
61 p->pid = getpid(); // 将server进程的pid号写入到共享内存
62 pause(); // 等待client读取到server pid号
63 pid=p->pid; // 获取client的进程号
64
65 while (1) {
66 //write share memory
67 printf("parent process begin to write memory data\n");
68 fgets(p->buf, 124, stdin);
69 kill(pid, SIGUSR1); // 向client发送信号通知client读取共享内存数据
70 pause(); // 等待client读取完共享内存数据
71 }
72
73 //在用户空间删除共享内存的地址
74 shmdt(p);
75
76 shmctl(shmid, IPC_RMID, NULL);
77 system("ipcs -m");
78 return 0;
79 }
client进程源码(参考jz2440\process_ipc\1st_shm\7th_shm_2.c)
01 /**********************************************************************
02 * 功能描述: 1.client进程使用ftok生成一个key值,利用这个key值打开一个共享内存
03 2.client进程收到server进程发送过来的信号之后,开始读取共享内存
04 3.子进程读完共享内存后,发送信号通知父进程读取完成
05 * 输入参数: 无
06 * 输出参数: 无
07 * 返 回 值: 无
08 * 修改日期 版本号 修改人 修改内容
09 * -----------------------------------------------
10 * 2020/05/16 V1.0 zh(ryan) 创建
11 ***********************************************************************/
12
13 #include <stdio.h>
14 #include <stdlib.h>
15 #include <unistd.h>
16 #include <sys/types.h>
17 #include <sys/shm.h>
18 #include <signal.h>
19 #include <string.h>
20
21 struct mybuf
22 {
23 int pid;
24 char buf[124];
25 };
26
27 void myfun(int signum)
28 {
29 return;
30 }
31
32 int main(int argc, char *argv[])
33 {
34 int shmid;
35 int key;
36 struct mybuf *p;
37 int pid;
38
39 key = ftok("./a.c", 'a');
40 if (key < 0) {
41 printf("create key fail\n");
42 return -1;
43 }
44 printf("create key sucess\n");
45
46 shmid = shmget(key, 128, IPC_CREAT | 0777);
47 if (shmid < 0) {
48 printf("create shared memory fail\n");
49 return -1;
50 }
51 printf("create shared memory sucess, shmid = %d\n", shmid);
52
53 signal(SIGUSR1, myfun);
54 p = (struct mybuf *)shmat(shmid, NULL, 0);
55 if (p == NULL) {
56 printf("shmat fail\n");
57 return -1;
58 }
59 printf("client process shmat sucess\n");
60
61 // get server pid
62 //read share memory
63 pid = p->pid;
64 // write client pid to share memory
65 p->pid = getpid();
66 kill(pid, SIGUSR2); // tell server process to read data
67
68 //client start to read share memory
69
70 while (1) {
71 pause(); // wait server process write share memory
72 printf("client process read data:%s\n", p->buf); // read data
73 kill(pid, SIGUSR2); // server can write share memory
74 }
75
76 //在用户空间删除共享内存的地址
77 shmdt(p);
78
79 shmctl(shmid, IPC_RMID, NULL);
80 system("ipcs -m");
81 return 0;
82 }
该源码留给读者自行实验,需要注意的是,因为这个时候需要运行两个console,分别运行server进程和client进程,可以采用一个串口console,一个telnet console。我们也可以在ubuntu下开启两个terminal验证。
4.4.2 消息队列
4.4.2.1 什么是消息队列
消息队列是消息的链表,它是一个链式队列,和管道类似,每个消息多有最大长度限制,可用cat/proc/sys/kernel/msgmax查看。
内核为每个消息队列对象维护了一个数据结构msgqid_ds,用于标识消息队列,以便让进程知道当前操作的是哪一个消息队列,每一个msqid_ds表示一个消息队列,并通过msqid_ds.msg_first、msg_last维护一个先进先出的msg链表队列,当发送一个消息到该消息队列时,把发送的消息构造成一个msg的结构对象,并添加到msqid_ds.msg_first、msg_last维护的链表队列。在内核中的表示如下:
4.4.2.2 特点
-
生命周期跟随内核,消息队列一直存在,需要用户显示调用接口删除或者使用命令删除。
-
消息队列可以实现双向通信
-
克服了管道只能承载无格式字节流的缺点
4.4.2.3 消息队列函数
msgget函数
创建或者打开消息队列的函数
头文件:#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
原型: int msgget(key_t key, int flag)
参数: key 和消息队列关联的key值
flag 消息队列的访问权限
返回值: 成功,消息队列ID,出错 -1
msgctl函数
消息队列控制函数
原型: int msgctl(int msgqid, int cmd, struct msqid_ds *buf)
参数: msgqid 消息队列ID
cmd IPC_STAT 读取消息队列的属性,并将其保存在buf指向的缓冲区中
IPC_SET 设置消息队列的属性,这个值取自buf参数
IPC_RMID 从系统中删除消息队列
buf 消息缓冲区
返回值: 成功 0,出错 -1
msgsnd函数
把一条消息添加到消息队列中
头文件#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
原型: int msgsnd(int msgqid, const void *msgp, size_t size, int flag)
参数: msgqid 消息队列ID
msgp 指向消息的指针,常用消息结构msgbuf如下
struct msgbuf {
long mtype; //消息类型
char mtext[N]; //消息正文
};
size 消息正文的字节数
flag IPC_NOWAIT 消息没有发送完成也会立即返回
0: 直到发送完成函数才会返回
返回值: 成功 0,出错 -1
msgrcv函数
从一个消息队列接受消息
原型: int msgrcv(int msgqid, void *msgp, size_t size, long msgtype, int flag)
参数: msgqid 消息队列ID
msgp 接收消息的缓冲区
size 要接收消息的字节数
msgtype 0 接收消息队列中第一个消息
大于0 接收消息队列中第一个类型为msgtype的消息
小于0 接收消息队列中类型值不大于msgtype的绝对值且类型值又最小的消息
flag IPC_NOWAIT 没有消息,会立即返回
0: 若无消息则会一直阻塞
返回值: 成功 接收消息的长度,出错 -1
4.4.2.4 消息队列实现进程间通信
server源码(参考jz2440\process_ipc\2nd_shm\write_msg.c)
01 /**********************************************************************
02 * 功能描述: 1.server进程向消息队列中写数据
03 * 输入参数: 无
04 * 输出参数: 无
05 * 返 回 值: 无
06 * 修改日期 版本号 修改人 修改内容
07 * -----------------------------------------------
08 * 2020/05/16 V1.0 zh(ryan) 创建
09 ***********************************************************************/
10
11 #include <stdio.h>
12 #include <stdlib.h>
13 #include <unistd.h>
14 #include <sys/types.h>
15 #include <sys/msg.h>
16 #include <signal.h>
17 #include <string.h>
18
19 struct msgbuf {
20 long type; //消息类型
21 char voltage[124]; //消息正文
22 char ID[4];
23 };
24
25 int main(int argc, char *argv[])
26 {
27 int msgid, readret, key;
28 struct msgbuf sendbuf;
29
30 key = ftok("./a.c", 'a');
31 if (key < 0){
32 printf("create key fail\n");
33 return -1;
34 }
35 msgid = msgget(key, IPC_CREAT|0777);
36 if (msgid < 0) {
37 printf("create msg queue fail\n");
38 return -1;
39 }
40 printf("create msg queue sucess, msgid = %d\n", msgid);
41 system("ipcs -q");
42
43 // write message queue
44 sendbuf.type = 100;
45 while(1) {
46 memset(sendbuf.voltage, 0, 124); //clear send buffer
47 printf("please input message:");
48 fgets(sendbuf.voltage, 124, stdin);
49 //start write msg to msg queue
50 msgsnd(msgid, (void *)&sendbuf, strlen(sendbuf.voltage), 0);
51 }
52
53 return 0;
54 }
client源码(参考jz2440\process_ipc\2nd_shm\read_msg.c)
01 /**********************************************************************
02 * 功能描述: 1.client进程从消息队列中读数据
03 * 输入参数: 无
04 * 输出参数: 无
05 * 返 回 值: 无
06 * 修改日期 版本号 修改人 修改内容
07 * -----------------------------------------------
08 * 2020/05/16 V1.0 zh(ryan) 创建
09 ***********************************************************************/
10
11 #include <stdio.h>
12 #include <stdlib.h>
13 #include <unistd.h>
14 #include <sys/types.h>
15 #include <sys/msg.h>
16 #include <signal.h>
17 #include <string.h>
18
19 struct msgbuf {
20 long type; //消息类型
21 char voltage[124]; //消息正文
22 char ID[4];
23 };
24
25 int main(int argc, char *argv[])
26 {
27 int msgid, key;
28 struct msgbuf readbuf;
29
30 key = ftok("./a.c", 'a');
31 if (key < 0){
32 printf("create key fail\n");
33 return -1;
34 }
35 msgid = msgget(key, IPC_CREAT|0777);
36 if (msgid < 0) {
37 printf("create msg queue fail\n");
38 return -1;
39 }
40 printf("create msg queue sucess, msgid = %d\n", msgid);
41 system("ipcs -q");
42
43 // read message queue
44 while(1) {
45 memset(readbuf.voltage, 0, 124); //clear recv buffer
46 //start read msg to msg queue
47 msgrcv(msgid, (void *)&readbuf, 124, 100, 0);
48 printf("recv data from message queue:%s", readbuf.voltage);
49 }
50
51 return 0;
52 }
JZ2440实验
- 编译
arm-linux-gcc write_msg.c -o write_msg
arm-linux-gcc read_msg.c -o read_msg
- 拷贝到NFS文件系统
cp write_msg /work/nfs_root/first_fs
cp read_msg /work/nfs_root/first_fs
- 运行
先在后台执行read_msg,然后在前台运行write_msg,此时在console下输入字符串,可以看到client进程能读到消息队列中的字符串 。
./read_msg &
./ write_msg
4.4.3 信号量灯
4.4.3.1 什么是P、V操作
当不同进程需要访问同一个资源时,由于不同进程的执行次序是未知的,有可能某个进程正在写该资源,而另一个进程正在读该资源,这样会造成进程执行的不确定性。这样的同一个资源,我们称为共享资源,共享资源一次只允许一个进程访问。因此进程在访问共享资源时,需要加上同步、互斥操作。
一般地,P操作表示申请该共享资源,V操作表示释放该共享资源。
4.4.3.2 什么是信号量灯
它是信号量的集合,包含多个信号量,可对多个信号灯同时进行P/V操作,主要用来实现进程、线程间同步/互斥。内核为每个信号量灯维护了一个数据结构semid_ds,用于标识信号量灯,以便进程知道当前操作的是哪个信号量灯,在内核中的表示如下所示。
它和POSIX规范中的信号量有什么区别呢?POSIX规范中的信号量只作用于一个信号量,而IPC对象中的信号量灯会作用于一组信号量。
功能 | 信号量(POSIX) | 信号量灯(IPC对象) |
---|---|---|
定义信号变量 | sem_t sem1 | semget |
初始化信号量 | sem_init | semctl |
P操作 | sem_wait | semop |
V操作 | sem_post | semop |
为什么需要IPC对象中的信号量灯呢?有POSIX规范中的信号量不够吗?
考虑如下场景:
-
线程A和线程B都需要访问共享资源1和共享资源2,在线程A中会需要先申请共享资源1,然后再申请共享资源2。
-
但是在线程B中,会先申请贡献资源2,然后再申请共享资源1。
-
当线程A中开始申请共享资源1时,紧接着会申请共享资源2;而此时线程B中开始申请共享资源2时,紧接着会申请共享资源1。
-
线程B正在占用着共享资源2,线程A正在占着共享资源1,导致线程B申请不到共享资源1,它就不会释放共享资源2;线程A申请不到共享资源2,它就不会释放共享资源1;这样就造成了死锁。
4.4.3.3 信号量灯函数
semget函数
创建或者打开函数
头文件#includde <sys/types.h>
#includde <sys/ipc.h>
#includde <sys/sem.h>
原型: int semget(key_t key, int nsems, int semflag)
参数: key 和信号灯集关联的key值
nsems 信号灯集包含的信号灯数目
semflag 信号灯集的访问权限
返回值: 成功,信号灯ID,出错 -1
semctl函数
信号量灯控制函数
头文件#includde <sys/types.h>
#includde <sys/ipc.h>
#includde <sys/sem.h>
原型: int semctl(int semid, int semnum, int cmd, ...union semun arg)
注意最后一个参数不是地址,可以有,可以没有
参数: semid 信号灯集id
semnum 要修改的信号灯集编号,删除操作时,这个值可以设置为任意值
cmd GETVAL 获取信号灯的值
SETVAL 设置信号灯的值
IPC_RMID 删除信号灯
union semun arg: union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO (Linux-specific) */
};
返回值: 成功,消息队列ID,出错 -1
semop函数
p/v操作函数
头文件#includde <sys/types.h>
#includde <sys/ipc.h>
#includde <sys/sem.h>
原型: int semop(int semid, struct sembuf *opsptr, size_t nops)
参数: semid 信号灯集id
opsptr struct sembuf{
short sem_num; //要操作信号灯的编号
short sem_op; //0: 等待,直到信号灯的值变为0,1:资源释放,V操作,-1:分配资源,P操作
short sem_flg; //0: IPC_NOWAIT, SEM_UNDO
}
nops 要操作信号灯个数
返回值: 成功,消息队列ID,出错 -1
4.4.3.4 信号量灯实现进程间同步/互斥
程序源码(参考jz2440\process_ipc\3rd_shm\share_sysv.c)
01 /**********************************************************************
02 * 功能描述: 1.父进程从键盘输入字符串到共享内存.
03 2.子进程删除字符串中的空格并打印.
04 3.父进程输入quit后删除共享内存和信号灯集,程序结束.
05 * 输入参数: 无
06 * 输出参数: 无
07 * 返 回 值: 无
08 * 修改日期 版本号 修改人 修改内容
09 * -----------------------------------------------
10 * 2020/05/16 V1.0 zh(ryan) 创建
11 ***********************************************************************/
12
13 #include <stdio.h>
14 #include <stdlib.h>
15 #include <string.h>
16 #include <sys/ipc.h>
17 #include <sys/sem.h>
18 #include <sys/types.h>
19 #include <sys/shm.h>
20 #include <signal.h>
21 #include <unistd.h>
22
23 #define N 64
24 #define READ 0
25 #define WRITE 1
26
27 union semun {
28 int val;
29 struct semid_ds *buf;
30 unsigned short *array;
31 struct seminfo *__buf;
32 };
33
34 void init_sem(int semid, int s[], int n)
35 {
36 int i;
37 union semun myun;
38
39 for (i = 0; i < n; i++){
40 myun.val = s[i];
41 semctl(semid, i, SETVAL, myun);
42 }
43 }
44
45 void pv(int semid, int num, int op)
46 {
47 struct sembuf buf;
48
49 buf.sem_num = num;
50 buf.sem_op = op;
51 buf.sem_flg = 0;
52 semop(semid, &buf, 1);
53 }
54
55 int main(int argc, char *argv[])
56 {
57 int shmid, semid, s[] = {0, 1};
58 pid_t pid;
59 key_t key;
60 char *shmaddr;
61
62 key = ftok(".", 's');
63 if (key == -1){
64 perror("ftok");
65 exit(-1);
66 }
67
68 shmid = shmget(key, N, IPC_CREAT|0666);
69 if (shmid < 0) {
70 perror("shmid");
71 exit(-1);
72 }
73
74 semid = semget(key, 2, IPC_CREAT|0666);
75 if (semid < 0) {
76 perror("semget");
77 goto __ERROR1;
78 }
79 init_sem(semid, s, 2);
80
81 shmaddr = shmat(shmid, NULL, 0);
82 if (shmaddr == NULL) {
83 perror("shmaddr");
84 goto __ERROR2;
85 }
86
87 pid = fork();
88 if(pid < 0) {
89 perror("fork");
90 goto __ERROR2;
91 } else if (pid == 0) {
92 char *p, *q;
93 while(1) {
94 pv(semid, READ, -1);
95 p = q = shmaddr;
96 while (*q) {
97 if (*q != ' ') {
98 *p++ = *q;
99 }
100 q++;
101 }
102 *p = '\0';
103 printf("%s", shmaddr);
104 pv(semid, WRITE, 1);
105 }
106 } else {
107 while (1) {
108 pv(semid, WRITE, -1);
109 printf("input > ");
110 fgets(shmaddr, N, stdin);
111 if (strcmp(shmaddr, "quit\n") == 0) break;
112 pv(semid, READ, 1);
113 }
114 kill(pid, SIGUSR1);
115 }
116
117 __ERROR2:
118 semctl(semid, 0, IPC_RMID);
119 __ERROR1:
120 shmctl(shmid, IPC_RMID, NULL);
121 return 0;
122 }
JZ2440实验
- 编译
arm-linux-gcc share_sysv.c -o share_sysv
- 拷贝到NFS文件系统
cp share_sysv /work/nfs_root/first_fs
- 运行
在父进程的console下输入字符串,此时子进程会读取到这个字符串。
./share_sysv
4.5 进程通信之信号通信
4.5.1 信号机制
-
一般地,中断主要是指硬件中断,比如GPIO中断、定时器中断,这些硬件中断时外设模块工作时,发送给CPU的,也是一种异步方式。
-
信号是软件层次上对中断机制的一种模拟,是一种异步通信方式。
-
Linux内核通过信号通知用户进程,不同的信号类型代表不同的事件。
-
Linux对早期的unix信号机制进行了扩展。
4.5.2 常见信号类型
4.5.3 信号发送函数
kill函数
头文件 #include <unistd.h>
#include <signal.h>
函数原型int kill(pid_t pid, int sig);
参数 pid : 指定接收进程的进程号
0代表同组进程;-1代表所有除了INIT进程和当前进程之外的进程
sig : 信号类型
返回值 成功返回0,失败返回EOF
raise函数
头文件 #include <unistd.h>
#include <signal.h>
函数原型int raise(int sig);
参数 sig : 信号类型
返回值 成功返回0,失败返回EOF
alarm函数
头文件 #include <unistd.h>
#include <signal.h>
函数原型 int alarm(unsigned int seconds);
参数 seconds 定时器的时间
返回值 成功返回上个定时器的剩余时间,失败返回EOF
pause函数
进程调用这个函数后会一直阻塞,直到而被信号中断,功能和sleep类似。
头文件 #include <unistd.h>
#include <signal.h>
函数原型 int pause(void);
返回值 成功返回0,失败返回EOF
signal函数
设置信号响应方式,请注意这个函数和kill、killall的区别,我们中文使用者会理解为发信号,实际上它并不是发信号。
头文件 #include <unistd.h>
#include <signal.h>
函数原型 void (*signal(int signo, void(*handler)(int)))(int)
参数 signo 要设置的信号类型
handler 指定的信号处理函数;
返回值 成功返回0,失败返回EOF
4.5.4 进程捕捉信号
程序源码(参考jz2440\process_single\send_single.c)
01 /**********************************************************************
02 * 功能描述: 1.捕捉终端发送过来的信号
03 * 输入参数: 无
04 * 输出参数: 无
05 * 返 回 值: 无
06 * 修改日期 版本号 修改人 修改内容
07 * -----------------------------------------------
08 * 2020/05/16 V1.0 zh(ryan) 创建
09 ***********************************************************************/
10
11 #include <stdio.h>
12 #include <stdlib.h>
13 #include <unistd.h>
14 #include <sys/types.h>
15 #include <signal.h>
16
17 void handler(int signo)
18 {
19 switch (signo) {
20 case SIGINT:
21 printf("I have got SIGINT\n");
22 break;
23
24 case SIGQUIT:
25 printf("I have got SIGQUIT\n");
26 break;
27
28 default:
29 printf("don't respond to this signal[%d]\n", signo);
30 exit(0);
31 }
32 }
33
34 int main(int argc, char *argv[])
35 {
36 signal(SIGINT, handler);
37 signal(SIGQUIT, handler);
38 while (1)
39 pause();
40 return 0;
41 }
JZ2440实验
- 编译
arm-linux-gcc send_single.c -o send_single
- 拷贝到NFS文件系统
cp send_single /work/nfs_root/first_fs
- 运行
./send_single
实际上在利用共享内存实现进程间通信时,我们已经使用到了信号通信,父进程写完共享内存后发送信号通知子进程,子进程收到信号后开始读共享内存,这里就不在给出两个进程之间使用信号通信的例子了,请读者参考共享内存中实现两个进程通信的代码。
4.6 进程通信之socket通信
4.6.1 什么是socket
先思考一个问题:网络环境中的进程如何实现通信?比如当我们使用QQ和好友聊天的时候,QQ进程是如何与服务器以及你好友所在的QQ进程之间通信的?这些靠的就是socket来实现的。
Socket起源于UNIX,Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。在《有名管道》那一节中,我们知道socket也是一种文件类型,只不过socket是一种伪文件,存在于内核缓冲区中,大小不变,一直是0。
socket文件一定是成对出现的,server端有一个套接字文件,client端也有一个套接字文件,每个进程需要和对应的套接字文件绑定,每个进程通过读写它的套接字文件,交由内核实现,如下所示。
一般地,socket用来实现网络环境中,不同主机上的进程通信,但是也可以用来在同一个主机上的不同进程之间通信,本小节主要探讨socket用在同一个主机上的不同进程间通信。
4.6.2 相关函数
socket函数
创建socket文件描述符函数
头文件#include <sys/types.h>
#include <sys/socket.h>
原型: int socket(int domain, int type, int protocol)
返回值: 成功,消息队列ID,出错 -1
bind函数
将socket文件描述符和某个socket文件绑定
头文件#include <sys/types.h>
#include <sys/socket.h>
原型: int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数: sockfd:利用系统调用socket()建立的套接字描述符
addr:代表需要绑定的本地地址信息
addrlen: 本地地址信息长度
返回值: 成功,消息队列ID,出错 -1
listen函数
设置监听某个socket文件描述符,设置能够同时和服务端连接的客户端数量,一般只有server会调用
头文件#include <sys/types.h>
#include <sys/socket.h>
原型: int listen(int sockfd, int backlog);
参数: sockfd:利用系统调用socket()建立的套接字描述符
backlog:server可以接受连接的最大client数量
返回值: 成功,消息队列ID,出错 -1
accept函数
等待client建立连接的函数,一般只有server会调用
头文件#include <sys/types.h>
#include <sys/socket.h>
原型: int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
参数: sockfd:利用系统调用socket()建立的套接字描述符
addr:指向已经建立连接的对端client地址信息的指针
addrlen: 对端client地址信息长度
返回值: 成功,消息队列ID,出错 -1
connet函数
client主动连接server函数,一般只有client才会调用
头文件#include <sys/types.h>
#include <sys/socket.h>
原型: int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数: sockfd:利用系统调用socket()建立的套接字描述符
addr:指向已经建立连接的对端server地址信息的指针
addrlen: 对端server地址信息长度
返回值: 成功,消息队列ID,出错 -1
send函数
发送数据函数
头文件#include <sys/types.h>
#include <sys/socket.h>
原型: ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数: sockfd:指向要发送数据的socket文件描述符,已经建立连接的文件描述符
buf: 存放要发送数据的缓冲区
len: 实际要发送数据的字节数
flags: 一般为0或者如下的宏
MSG_DONTROUTE 绕过路由表查找
MSG_DONTWAIT 仅本操作非阻塞
MSG_OOB 发送或接收带外数据
MSG_PEEK 窥看外来消息
MSG_WAITALL 等待所有数据
返回值: 成功,消息队列ID,出错 -1
recv函数
接收数据函数
头文件#include <sys/types.h>
#include <sys/socket.h>
原型: ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数: sockfd:已经建立连接的文件描述符
buf: 存放要接收数据的缓冲区
len: 实际要接收数据的字节数
flags:一般为0或者如下的宏
MSG_DONTROUTE 绕过路由表查找
MSG_DONTWAIT 仅本操作非阻塞
MSG_OOB 发送或接收带外数据
MSG_PEEK 窥看外来消息
MSG_WAITALL 等待所有数据
返回值: 成功,消息队列ID,出错 -1
4.6.3 socket实现进程间通信
程序实现一般步骤
Server端
1.创建socket
2.绑定socket
3.设置监听
4.等待客户端连接
5.发送/接收数据
Client端
1.创建socket
2.绑定socket
3.连接
4.发送/接收数据
server源码(参考jz2440\process_socket\server.c)
01 /**********************************************************************
02 * 功能描述: 1.server打印client发送过来的字符串,并将该字符串回发给client
03 * 输入参数: 无
04 * 输出参数: 无
05 * 返 回 值: 无
06 * 修改日期 版本号 修改人 修改内容
07 * -----------------------------------------------
08 * 2020/05/16 V1.0 zh(ryan) 创建
09 ***********************************************************************/
10
11 #include <stdio.h>
12 #include <stdlib.h>
13 #include <unistd.h>
14 #include <sys/types.h>
15 #include <sys/stat.h>
16 #include <string.h>
17 #include <arpa/inet.h>
18 #include <sys/un.h>
19
20 int main(int argc, char *argv[])
21 {
22 int lfd ,ret, cfd;
23 struct sockaddr_un serv, client;
24 socklen_t len = sizeof(client);
25 char buf[1024] = {0};
26 int recvlen;
27
28 //创建socket
29 lfd = socket(AF_LOCAL, SOCK_STREAM, 0);
30 if (lfd == -1) {
31 perror("socket error");
32 return -1;
33 }
34
35 //如果套接字文件存在,删除套接字文件
36 unlink("server.sock");
37
38 //初始化server信息
39 serv.sun_family = AF_LOCAL;
40 strcpy(serv.sun_path, "server.sock");
41
42 //绑定
43 ret = bind(lfd, (struct sockaddr *)&serv, sizeof(serv));
44 if (ret == -1) {
45 perror("bind error");
46 return -1;
47 }
48
49 //设置监听,设置能够同时和服务端连接的客户端数量
50 ret = listen(lfd, 36);
51 if (ret == -1) {
52 perror("listen error");
53 return -1;
54 }
55
56 //等待客户端连接
57 cfd = accept(lfd, (struct sockaddr *)&client, &len);
58 if (cfd == -1) {
59 perror("accept error");
60 return -1;
61 }
62 printf("=====client bind file:%s\n", client.sun_path);
63
64 while (1) {
65 recvlen = recv(cfd, buf, sizeof(buf), 0);
66 if (recvlen == -1) {
67 perror("recv error");
68 return -1;
69 } else if (recvlen == 0) {
70 printf("client disconnet...\n");
71 close(cfd);
72 break;
73 } else {
74 printf("server recv buf: %s\n", buf);
75 send(cfd, buf, recvlen, 0);
76 }
77 }
78
79 close(cfd);
80 close(lfd);
81 return 0;
82 }
client源码(参考jz2440\process_socket\client.c)
01 /**********************************************************************
02 * 功能描述: 1.client从标准输入获取到一个字符串,然后将这个字符串发送给server
03 * 输入参数: 无
04 * 输出参数: 无
05 * 返 回 值: 无
06 * 修改日期 版本号 修改人 修改内容
07 * -----------------------------------------------
08 * 2020/05/16 V1.0 zh(ryan) 创建
09 ***********************************************************************/
10
11 #include <stdio.h>
12 #include <stdlib.h>
13 #include <unistd.h>
14 #include <sys/types.h>
15 #include <sys/stat.h>
16 #include <string.h>
17 #include <arpa/inet.h>
18 #include <sys/un.h>
19
20 int main(int argc, char *argv[])
21 {
22 int lfd ,ret;
23 struct sockaddr_un serv, client;
24 socklen_t len = sizeof(client);
25 char buf[1024] = {0};
26 int recvlen;
27
28 //创建socket
29 lfd = socket(AF_LOCAL, SOCK_STREAM, 0);
30 if (lfd == -1) {
31 perror("socket error");
32 return -1;
33 }
34
35 //如果套接字文件存在,删除套接字文件
36 unlink("client.sock");
37
38 //给客户端绑定一个套接字文件
39 client.sun_family = AF_LOCAL;
40 strcpy(client.sun_path, "client.sock");
41 ret = bind(lfd, (struct sockaddr *)&client, sizeof(client));
42 if (ret == -1) {
43 perror("bind error");
44 return -1;
45 }
46
47 //初始化server信息
48 serv.sun_family = AF_LOCAL;
49 strcpy(serv.sun_path, "server.sock");
50 //连接
51 connect(lfd, (struct sockaddr *)&serv, sizeof(serv));
52
53 while (1) {
54 fgets(buf, sizeof(buf), stdin);
55 send(lfd, buf, strlen(buf)+1, 0);
56
57 recv(lfd, buf, sizeof(buf), 0);
58 printf("client recv buf: %s\n", buf);
59 }
60
61 close(lfd);
62 return 0;
63 }
JZ2440实验
- 编译
arm-linux-gcc server.c -o server
arm-linux-gcc client.c -o client
- 拷贝到NFS文件系统
cp server /work/nfs_root/first_fs
cp client /work/nfs_root/first_fs
- 运行
为方便看程序运行结果,server在后台执行;client在前台运行,client能够接收来自终端的输入。
./server &
./client
4.6.4 一个server和多个client之间的通信
在实际项目开发中, 更常见的一种场景是:一个server要和多个client之间通信,这部分实现方式交给读者自行实现,实现的方式有很多,比如如下两种方式,当然还要其他方法。
-
多进程实现,一个主进程用来实现检测client的连接,每检测一次client连接,则为这个client创建一个专门的进程,用于实现两者间通信。
-
也可以使用多线程实现,一个主线程用来检测client的连接,每检测一次client连接,则为这个client创建一个专门的线程,用于实现两者间通信。
第五章:多线程编程
线程编程
5 线程编程
本章将分为两大部分进行讲解,前半部分将引出线程的使用场景及基本概念,通过示例代码来说明一个线程创建到退出到回收的基本流程。后半部分则会通过示例代码来说明如果控制好线程,从临界资源访问与线程的执行顺序控制上引出互斥锁、信号量的概念与使用方法。
5.1 线程的使用
5.1.1 为什么要使用多线程
在编写代码时,是否会遇到以下的场景会感觉到难以下手?
场景一:写程序在拷贝文件时,需要一边去拷贝文件,一边去向用户展示拷贝文件的进度时,传统做法是通过每次拷贝完成结束后去更新变量,再将变量转化为进度显示出来。其中经历了拷贝->计算->显示->拷贝->计算->显示...直至拷贝结束。这样的程序架构及其的低效,必须在单次拷贝结束后才可以刷新当前拷贝进度,若可以将进程分支,一支单独的解决拷贝问题,一支单独的解决计算刷新问题,则程序效率会提升很多。
场景二:用阻塞方式去读取数据,实时需要发送数据的时候。例如在进行串口数据传输或者网络数据传输的时候,我们往往需要双向通信,当设置读取数据为阻塞模式时候,传统的单线程只能等到数据接收来临后才能冲过阻塞,再根据逻辑进行发送。当我们要实现随时发送、随时接收时,无法满足我们的业务需求。若可以将进程分支,一支单纯的处理接收数据逻辑,一支单纯的解决发送数据逻辑,就可以完美的实现功能。
基于以上场景描述,多线程编程可以完美的解决上述问题。
5.1.2 线程概念
所谓线程,就是操作系统所能调度的最小单位。普通的进程,只有一个线程在执行对应的逻辑。我们可以通过多线程编程,使一个进程可以去执行多个不同的任务。相比多进程编程而言,线程享有共享资源,即在进程中出现的全局变量,每个线程都可以去访问它,与进程共享“4G”内存空间,使得系统资源消耗减少。本章节来讨论Linux下POSIX线程。
5.1.3 线程的标识pthread_t
对于进程而言,每一个进程都有一个唯一对应的PID号来表示该进程,而对于线程而言,也有一个“类似于进程的PID号”,名为tid,其本质是一个pthread_t类型的变量。线程号与进程号是表示线程和进程的唯一标识,但是对于线程号而言,其仅仅在其所属的进程上下文中才有意义。
获取线程号
#include <pthread.h>
pthread_t pthread_self(void);
成功:返回线程号
在程序中,可以通过函数pthread_self,来返回当前线程的线程号,例程1给出了打印线程tid号。
测试例程1:(Phtread_txex1.c)
1 #include <pthread.h>
2 #include <stdio.h>
3
4 int main()
5 {
6 pthread_t tid = pthread_self();//获取主线程的tid号
7 printf("tid = %lu\n",(unsigned long)tid);
8 return 0;
9 }
注意:因采用POSIX线程接口,故在要编译的时候包含pthread库,使用gcc编译应gcc xxx.c -lpthread 方可编译多线程程序。
编译结果:
5.1.4 线程的创建
创建线程
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
成功:返回0
在传统的程序中,一个进程只有一个线程,可以通过函数pthread_create来创建线程。
该函数第一个参数为pthread_t类型的线程号地址,当函数执行成功后会指向新建线程的线程号;第二个参数表示了线程的属性,一般传入NULL表示默认属性;第三个参数代表返回值为void *,形参为void *的函数指针,当线程创建成功后,会自动的执行该回调函数;第四个参数则表示为向线程处理函数传入的参数,若不传入,可用NULL填充,有关线程传参后续小节会有详细的说明,接下来通过一个简单例程来使用该函数创建出一个线程。
测试例程2:(Phtread_txex2.c)
1 #include <pthread.h>
2 #include <stdio.h>
3 #include <unistd.h>
4 #include <errno.h>
5
6 void *fun(void *arg)
7 {
8 printf("pthread_New = %lu\n",(unsigned long)pthread_self());//打印线程的tid号
9 }
10
11 int main()
12 {
13
14 pthread_t tid1;
15 int ret = pthread_create(&tid1,NULL,fun,NULL);//创建线程
16 if(ret != 0){
17 perror("pthread_create");
18 return -1;
19 }
20
21 /*tid_main 为通过pthread_self获取的线程ID,tid_new通过执行pthread_create成功后tid指向的空间*/
22 printf("tid_main = %lu tid_new = %lu \n",(unsigned long)pthread_self(),(unsigned long)tid1);
23
24 /*因线程执行顺序随机,不加sleep可能导致主线程先执行,导致进程结束,无法执行到子线程*/
25 sleep(1);
26
27 return 0;
28 }
29
运行结果:
通过pthread_create确实可以创建出来线程,主线程中执行pthread_create后的tid指向了线程号空间,与子线程通过函数pthread_self打印出来的线程号一致。
特别说明的是,当主线程伴随进程结束时,所创建出来的线程也会立即结束,不会继续执行。并且创建出来的线程的执行顺序是随机竞争的,并不能保证哪一个线程会先运行。可以将上述代码中sleep函数进行注释,观察实验现象。
去掉上述代码25行后运行结果:
上述运行代码3次,其中有2次被进程结束,无法执行到子线程的逻辑,最后一次则执行到了子线程逻辑后结束的进程。如此可以说明,线程的执行顺序不受控制,且整个进程结束后所产生的线程也随之被释放,在后续内容中将会描述如何控制线程执行。
5.1.5 向线程传入参数
pthread_create()的最后一个参数的为void *类型的数据,表示可以向线程传递一个void *数据类型的参数,线程的回调函数中可以获取该参数,例程3举例了如何向线程传入变量地址与变量值。
测试例程3:(Phtread_txex3.c)
1 #include <pthread.h>
2 #include <stdio.h>
3 #include <unistd.h>
4 #include <errno.h>
5
6 void *fun1(void *arg)
7 {
8 printf("%s:arg = %d Addr = %p\n",__FUNCTION__,*(int *)arg,arg);
9 }
10
11 void *fun2(void *arg)
12 {
13 printf("%s:arg = %d Addr = %p\n",__FUNCTION__,(int)(long)arg,arg);
14 }
15
16 int main()
17 {
18
19 pthread_t tid1,tid2;
20 int a = 50;
21 int ret = pthread_create(&tid1,NULL,fun1,(void *)&a);//创建线程传入变量a的地址
22 if(ret != 0){
23 perror("pthread_create");
24 return -1;
25 }
27 ret = pthread_create(&tid2,NULL,fun2,(void *)(long)a);//创建线程传入变量a的值
28 if(ret != 0){
29 perror("pthread_create");
30 return -1;
31 }
32 sleep(1);
33 printf("%s:a = %d Add = %p \n",__FUNCTION__,a,&a);
34 return 0;
35 }
36
运行结果:
本例程展示了如何利用线程创建函数的第四个参数向线程传入数据,举例了如何以地址的方式传入值、以变量的方式传入值,例程代码的21行,是将变量a先行取地址后,再次强制类型转化为void后传入线程,线程处理的回调函数中,先将万能指针void *转化为int *,再次取地址就可以获得该地址变量的值,其本质在于地址的传递。例程代码的27行,直接将int类型的变量强制转化为void *进行传递(针对不同位数机器,指针对其字数不同,需要int转化为long在转指针,否则可能会发生警告),在线程处理回调函数中,直接将void *数据转化为int类型即可,本质上是在传递变量a的值。
上述两种方法均可得到所要的值,但是要注意其本质,一个为地址传递,一个为值的传递。当变量发生改变时候,传递地址后,该地址所对应的变量也会发生改变,但传入变量值的时候,即使地址指针所指的变量发生变化,但传入的为变量值,不会受到指针的指向的影响,实际项目中切记两者之间的区别。具体说明见例程4.
测试例程4:(Phtread_txex4.c)
1 #include <pthread.h>
2 #include <stdio.h>
3 #include <unistd.h>
4 #include <errno.h>
5
6 void *fun1(void *arg)
7 {
8 while(1){
9
10 printf("%s:arg = %d Addr = %p\n",__FUNCTION__,*(int *)arg,arg);
11 sleep(1);
12 }
13 }
14
15 void *fun2(void *arg)
16 {
17 while(1){
18
19 printf("%s:arg = %d Addr = %p\n",__FUNCTION__,(int)(long)arg,arg);
20 sleep(1);
21 }
22 }
23
24 int main()
25 {
26
27 pthread_t tid1,tid2;
28 int a = 50;
29 int ret = pthread_create(&tid1,NULL,fun1,(void *)&a);
30 if(ret != 0){
31 perror("pthread_create");
32 return -1;
33 }
34 sleep(1);
35 ret = pthread_create(&tid2,NULL,fun2,(void *)(long)a);
36 if(ret != 0){
37 perror("pthread_create");
38 return -1;
39 }
40 while(1){
41 a++;
42 sleep(1);
43 printf("%s:a = %d Add = %p \n",__FUNCTION__,a,&a);
44 }
45 return 0;
46 }
47
运行结果:
上述例程讲述了如何向线程传递一个参数,在处理实际项目中,往往会遇到传递多个参数的问题,我们可以通过结构体来进行传递,解决此问题。
测试例程5:(Phtread_txex5.c)
1 #include <pthread.h>
2 #include <stdio.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <errno.h>
6
7 struct Stu{
8 int Id;
9 char Name[32];
10 float Mark;
11 };
12
13 void *fun1(void *arg)
14 {
15 struct Stu *tmp = (struct Stu *)arg;
16 printf("%s:Id = %d Name = %s Mark = %.2f\n",__FUNCTION__,tmp->Id,tmp->Name,tmp->Mark);
17
18 }
19
20 int main()
21 {
22
23 pthread_t tid1,tid2;
24 struct Stu stu;
25 stu.Id = 10000;
26 strcpy(stu.Name,"ZhangSan");
27 stu.Mark = 94.6;
28
29 int ret = pthread_create(&tid1,NULL,fun1,(void *)&stu);
30 if(ret != 0){
31 perror("pthread_create");
32 return -1;
33 }
34 printf("%s:Id = %d Name = %s Mark = %.2f\n",__FUNCTION__,stu.Id,stu.Name,stu.Mark);
35 sleep(1);
36 return 0;
37 }
38
运行结果:
5.1.6 线程的退出与回收
线程的退出情况有三种:第一种是进程结束,进程中所有的线程也会随之结束。第二种是通过函数pthread_exit来主动的退出线程。第三种通过函数pthread_cancel被其他线程被动结束。当线程结束后,主线程可以通过函数pthread_join/pthread_tryjoin_np来回收线程的资源,并且获得线程结束后需要返回的数据。
线程退出
#include <pthread.h>
void pthread_exit(void *retval);
该函数为线程退出函数,在退出时候可以传递一个void*类型的数据带给主线程,若选择不传出数据,可将参数填充为NULL。
线程资源回收(阻塞)
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
成功:返回0
该函数为线程回收函数,默认状态为阻塞状态,直到成功回收线程后被冲开阻塞。第一个参数为要回收线程的tid号,第二个参数为线程回收后接受线程传出的数据。
线程资源回收(非阻塞)
#define _GNU_SOURCE
#include <pthread.h>
int pthread_tryjoin_np(pthread_t thread, void **retval);
成功:返回0
该函数为非阻塞模式回收函数,通过返回值判断是否回收掉线程,成功回收则返回0,其余参数与pthread_join一致。
线程退出(指定线程号)
#include <pthread.h>
int pthread_cancel(pthread_t thread);
成功:返回0
该函数传入一个tid号,会强制退出该tid所指向的线程,若成功执行会返回0。
上述描述简单的介绍了有关线程回收的API,下面通过例程来说明上述API。
测试例程6:(Phtread_txex6.c)
1 #include <pthread.h>
2 #include <stdio.h>
3 #include <unistd.h>
4 #include <errno.h>
5
6 void *fun1(void *arg)
7 {
8 static int tmp = 0;//必须要static修饰,否则pthread_join无法获取到正确值
9 //int tmp = 0;
10 tmp = *(int *)arg;
11 tmp+=100;
12 printf("%s:Addr = %p tmp = %d\n",__FUNCTION__,&tmp,tmp);
13 pthread_exit((void *)&tmp);//将变量tmp取地址转化为void*类型传出
14 }
15
16
17 int main()
18 {
19
20 pthread_t tid1;
21 int a = 50;
22 void *Tmp = NULL;//因pthread_join第二个参数为void**类型
23 int ret = pthread_create(&tid1,NULL,fun1,(void *)&a);
24 if(ret != 0){
25 perror("pthread_create");
26 return -1;
27 }
28 pthread_join(tid1,&Tmp);
29 printf("%s:Addr = %p Val = %d\n",__FUNCTION__,Tmp,*(int *)Tmp);
30 return 0;
31 }
32
运行结果:
上述例程先通过23行将变量以地址的形式传入线程,在线程中做出了自加100的操作,当线程退出的时候通过线程传参,用void*类型的数据通过pthread_join接受。此例程去掉了之前加入的sleep函数,原因是pthread_join函数具备阻塞的特性,直至成功收回掉线程后才会冲破阻塞,因此不需要靠考虑主线程会执行到30行结束进程的情况。特别要说明的是例程第8行,当变量从线程传出的时候,需要加static修饰,对生命周期做出延续,否则无法传出正确的变量值。
测试例程7:(Phtread_txex7.c)
1 #define _GNU_SOURCE
2 #include <pthread.h>
3 #include <stdio.h>
4 #include <unistd.h>
5 #include <errno.h>
6
7 void *fun(void *arg)
8 {
9 printf("Pthread:%d Come !\n",(int )(long)arg+1);
10 pthread_exit(arg);
11 }
12
13
14 int main()
15 {
16 int ret,i,flag = 0;
17 void *Tmp = NULL;
18 pthread_t tid[3];
19 for(i = 0;i < 3;i++){
20 ret = pthread_create(&tid[i],NULL,fun,(void *)(long)i);
21 if(ret != 0){
22 perror("pthread_create");
23 return -1;
24 }
25 }
26 while(1){//通过非阻塞方式收回线程,每次成功回收一个线程变量自增,直至3个线程全数回收
27 for(i = 0;i <3;i++){
28 if(pthread_tryjoin_np(tid[i],&Tmp) == 0){
29 printf("Pthread : %d exit !\n",(int )(long )Tmp+1);
30 flag++;
31 }
32 }
33 if(flag >= 3) break;
34 }
35 return 0;
36 }
37
运行结果:
例程7展示了如何使用非阻塞方式来回收线程,此外也展示了多个线程可以指向同一个回调函数的情况。例程6通过阻塞方式回收线程几乎规定了线程回收的顺序,若最先回收的线程未退出,则一直会被阻塞,导致后续先退出的线程无法及时的回收。
通过函数pthread_tryjoin_np,使用非阻塞回收,线程可以根据退出先后顺序自由的进行资源的回收。
测试例程8:(Phtread_txex8.c)
1 #define _GNU_SOURCE
2 #include <pthread.h>
3 #include <stdio.h>
4 #include <unistd.h>
5 #include <errno.h>
6
7 void *fun1(void *arg)
8 {
9 printf("Pthread:1 come!\n");
10 while(1){
11 sleep(1);
12 }
13 }
14
15 void *fun2(void *arg)
16 {
17 printf("Pthread:2 come!\n");
18 pthread_cancel((pthread_t )(long)arg);//杀死线程1,使之强制退出
19 pthread_exit(NULL);
20 }
21
22 int main()
23 {
24 int ret,i,flag = 0;
25 void *Tmp = NULL;
26 pthread_t tid[2];
27 ret = pthread_create(&tid[0],NULL,fun1,NULL);
28 if(ret != 0){
29 perror("pthread_create");
30 return -1;
31 }
32 sleep(1);
33 ret = pthread_create(&tid[1],NULL,fun2,(void *)tid[0]);//传输线程1的线程号
34 if(ret != 0){
35 perror("pthread_create");
36 return -1;
37 }
38 while(1){//通过非阻塞方式收回线程,每次成功回收一个线程变量自增,直至2个线程全数回收
39 for(i = 0;i <2;i++){
40 if(pthread_tryjoin_np(tid[i],NULL) == 0){
41 printf("Pthread : %d exit !\n",i+1);
42 flag++;
43 }
44 }
45 if(flag >= 2) break;
46 }
47 return 0;
48 }
49
运行结果:
例程8展示了如何利用pthread_cancel函数主动的将某个线程结束。27行与33行创建了线程,将第一个线程的线程号传参形式传入了第二个线程。第一个的线程执行死循环睡眠逻辑,理论上除非进程结束,其永远不会结束,但在第二个线程中调用了pthread_cancel函数,相当于向该线程发送一个退出的指令,导致线程被退出,最终资源被非阻塞回收掉。此例程要注意第32行的sleep函数,一定要确保线程1先执行,因线程是无序执行,故加入该睡眠函数控制顺序,在本章后续,会讲解通过加锁、信号量等手段来合理的控制线程的临界资源访问与线程执行顺序控制。
5.2 线程的控制
5.2.1 多线程编临界资源访问
当线程在运行过程中,去操作公共资源,如全局变量的时候,可能会发生彼此“矛盾”现象。例如线程1企图想让变量自增,而线程2企图想要变量自减,两个线程存在互相竞争的关系导致变量永远处于一个“平衡状态”,两个线程互相竞争,线程1得到执行权后将变量自加,当线程2得到执行权后将变量自减,变量似乎永远在某个范围内浮动,无法到达期望数值,如例程9所示。
测试例程9:(Phtread_txex9.c)
1 #define _GNU_SOURCE
2 #include <pthread.h>
3 #include <stdio.h>
4 #include <unistd.h>
5 #include <errno.h>
6
7
8 int Num = 0;
9
10 void *fun1(void *arg)
11 {
12 while(Num < 3){
13 Num++;
14 printf("%s:Num = %d\n",__FUNCTION__,Num);
15 sleep(1);
16 }
17 pthread_exit(NULL);
18 }
19
20 void *fun2(void *arg)
21 {
22 while(Num > -3){
23 Num--;
24 printf("%s:Num = %d\n",__FUNCTION__,Num);
25 sleep(1);
26 }
27 pthread_exit(NULL);
28 }
29
30 int main()
31 {
32 int ret;
33 pthread_t tid1,tid2;
34 ret = pthread_create(&tid1,NULL,fun1,NULL);
35 if(ret != 0){
36 perror("pthread_create");
37 return -1;
38 }
39 ret = pthread_create(&tid2,NULL,fun2,NULL);
40 if(ret != 0){
41 perror("pthread_create");
42 return -1;
43 }
44 pthread_join(tid1,NULL);
45 pthread_join(tid2,NULL);
46 return 0;
47 }
48
运行结果:
为了解决上述对临界资源的竞争问题,pthread线程引出了互斥锁来解决临界资源访问。通过对临界资源加锁来保护资源只被单个线程操作,待操作结束后解锁,其余线程才可获得操作权。
5.2.2 互斥锁API简述
初始化互斥锁
#include <pthread.h>
int pthread_mutex_init(phtread_mutex_t *mutex,
const pthread_mutexattr_t *restrict attr);
成功:返回0
该函数作用为初始化一个互斥锁,一般情况申请一个全局的pthread_mutex_t类型的互斥锁变量,通过此函数完成锁内的初始化,第一个函数将该变量的地址传入,第二个参数为控制互斥锁的属性,一般为NULL。当函数成功后会返回0,代表初始化互斥锁成功。当然初始化互斥锁也可以调用宏来快速初始化:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITALIZER;
互斥锁加锁(阻塞)/解锁
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
lock函数与unlock函数分别为加锁解锁函数,只需要传入已经初始化好的pthread_mutex_t互斥锁变量,成功后会返回0。当某一个线程获得了执行权后,执行lock函数,一旦加锁成功后,其余线程遇到lock函数时候会发生阻塞,直至获取资源的线程执行unlock函数后,获得第二执行权的线程的阻塞模式被从开,同时也获取了lock,导致其余线程同样在阻塞,直至执行unlock被解锁。
特别注意的是,当获取lock之后,必须在逻辑处理结束后执行unlock,否则会发生死锁现象!导致其余线程一直处于阻塞状态,无法执行下去。在使用互斥锁的时候,尤其要注意使用pthread_cancel函数,防止发生死锁现象!
互斥锁加锁(非阻塞)
#include <pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex);
成功:返回0
该函数同样也是一个线程加锁函数,但该函数是非阻塞模式通过返回值来判断是否加锁成功,用法与上述阻塞加速函数一致。
互斥锁销毁
#include <pthread.h>
int pthread_mutex_destory(pthread_mutex_t *mutex);
成功:返回0
该函数是用于销毁互斥锁的,传入互斥锁的地址,就可以完成互斥锁的销毁,成功返回0。
测试例程10:(Phtread_txex10.c)
1 #define _GNU_SOURCE
2 #include <pthread.h>
3 #include <stdio.h>
4 #include <unistd.h>
5 #include <errno.h>
6
7 pthread_mutex_t mutex;//互斥锁变量 一般申请全局变量
8
9 int Num = 0;//公共临界变量
10
11 void *fun1(void *arg)
12 {
13 pthread_mutex_lock(&mutex);//加锁 若有线程获得锁,则会阻塞
14 while(Num < 3){
15 Num++;
16 printf("%s:Num = %d\n",__FUNCTION__,Num);
17 sleep(1);
18 }
19 pthread_mutex_unlock(&mutex);//解锁
20 pthread_exit(NULL);//线程退出 pthread_join会回收资源
21 }
22
23 void *fun2(void *arg)
24 {
25 pthread_mutex_lock(&mutex);//加锁 若有线程获得锁,则会阻塞
26 while(Num > -3){
27 Num--;
28 printf("%s:Num = %d\n",__FUNCTION__,Num);
29 sleep(1);
30 }
31 pthread_mutex_unlock(&mutex);//解锁
32 pthread_exit(NULL);//线程退出 pthread_join会回收资源
33 }
34
35 int main()
36 {
37 int ret;
38 pthread_t tid1,tid2;
39 ret = pthread_mutex_init(&mutex,NULL);//初始化互斥锁
40 if(ret != 0){
41 perror("pthread_mutex_init");
42 return -1;
43 }
44 ret = pthread_create(&tid1,NULL,fun1,NULL);//创建线程1
45 if(ret != 0){
46 perror("pthread_create");
47 return -1;
48 }
49 ret = pthread_create(&tid2,NULL,fun2,NULL);//创建线程2
50 if(ret != 0){
51 perror("pthread_create");
52 return -1;
53 }
54 pthread_join(tid1,NULL);//阻塞回收线程1
55 pthread_join(tid2,NULL);//阻塞回收线程2
56 pthread_mutex_destroy(&mutex);//销毁互斥锁
57 return 0;
58 }
59
运行结果:
上述例程通过加入互斥锁,保证了临界变量某一时刻只被某一线程控制,实现了临界资源的控制。需要说明的是,线程加锁在循环内与循环外的情况。本历程在进入while循环前进行了加锁操作,在循环结束后进行的解锁操作,如果将加锁解锁全部放入while循环内,作为单核的机器,执行结果无异,当有多核机器执行代码时,可能会发生“抢锁”现象,这取决于操作系统底层的实现。
5.2.3 多线程编执行顺序控制
解决了临界资源的访问,但似乎对线程的执行顺序无法得到控制,因线程都是无序执行,之前采用sleep强行延时的方法勉强可以控制执行顺序,但此方法在实际项目情况往往是不可取的,其仅仅可解决线程创建的顺序,当创建之后执行的顺序又不会受到控制,于是便引入了信号量的概念,解决线程执行顺序。
例程11将展示线程的执行的随机性。
测试例程11:(Phtread_txex11.c)
1 #define _GNU_SOURCE
2 #include <pthread.h>
3 #include <stdio.h>
4 #include <unistd.h>
5 #include <errno.h>
6
7 void *fun1(void *arg)
8 {
9 printf("%s:Pthread Come!\n",__FUNCTION__);
10 pthread_exit(NULL);
11 }
12
13 void *fun2(void *arg)
14 {
15 printf("%s:Pthread Come!\n",__FUNCTION__);
16 pthread_exit(NULL);
17 }
18
19 void *fun3(void *arg)
20 {
21 printf("%s:Pthread Come!\n",__FUNCTION__);
22 pthread_exit(NULL);
23 }
24
25 int main()
26 {
27 int ret;
28 pthread_t tid1,tid2,tid3;
29 ret = pthread_create(&tid1,NULL,fun1,NULL);
30 if(ret != 0){
31 perror("pthread_create");
32 return -1;
33 }
34 ret = pthread_create(&tid2,NULL,fun2,NULL);
35 if(ret != 0){
36 perror("pthread_create");
37 return -1;
38 }
39 ret = pthread_create(&tid3,NULL,fun3,NULL);
40 if(ret != 0){
41 perror("pthread_create");
42 return -1;
43 }
44 pthread_join(tid1,NULL);
45 pthread_join(tid2,NULL);
46 pthread_join(tid3,NULL);
47 return 0;
48 }
49
运行结果:
通过上述例程可以发现,多次执行该函数其次序是无序的,线程之间的竞争无法控制,通过使用信号量来使得线程顺序为可控的。
5.2.4 信号量API简述
初始化信号量
#include <semaphore.h>
int sem_init(sem_t *sem,int pshared,unsigned int value);
成功:返回0
该函数可以初始化一个信号量,第一个参数传入sem_t类型的地址,第二个参数传入0代表线程控制,否则为进程控制,第三个参数表示信号量的初始值,0代表阻塞,1代表运行。待初始化结束信号量后,若执行成功会返回0。
信号量PV操作(阻塞)
#include <pthread.h>
int sem_wait(sem_t *sem);
int sem_post(sem_t *sem);
成功:返回0
sem_wait函数作用为检测指定信号量是否有资源可用,若无资源可用会阻塞等待,若有资源可用会自动的执行“sem-1”的操作。所谓的“sem-1”是与上述初始化函数中第三个参数值一致,成功执行会返回0.
sem_post函数会释放指定信号量的资源,执行“sem+1”操作。
通过以上2个函数可以完成所谓的PV操作,即信号量的申请与释放,完成对线程执行顺序的控制。
信号量申请资源(非阻塞)
#include <pthread.h>
int sem_trywait(sem_t *sem);
成功:返回0
与互斥锁一样,此函数是控制信号量申请资源的非阻塞函数,功能与sem_wait一致,唯一区别在于此函数为非阻塞。
信号量销毁
#include <pthread.h>
int sem_destory(sem_t *sem);
成功:返回0
该函数为信号量销毁函数,执行过后可将申请的信号量进行销毁。
测试例程12:(Phtread_txex12.c)
1 #define _GNU_SOURCE
2 #include <pthread.h>
3 #include <stdio.h>
4 #include <unistd.h>
5 #include <errno.h>
6 #include <semaphore.h>
7
8 sem_t sem1,sem2,sem3;//申请的三个信号量变量
9
10 void *fun1(void *arg)
11 {
12 sem_wait(&sem1);//因sem1本身有资源,所以不被阻塞 获取后sem1-1 下次会会阻塞
13 printf("%s:Pthread Come!\n",__FUNCTION__);
14 sem_post(&sem2);// 使得sem2获取到资源
15 pthread_exit(NULL);
16 }
17
18 void *fun2(void *arg)
19 {
20 sem_wait(&sem2);//因sem2在初始化时无资源会被阻塞,直至14行代码执行 不被阻塞 sem2-1 下次会阻塞
21 printf("%s:Pthread Come!\n",__FUNCTION__);
22 sem_post(&sem3);// 使得sem3获取到资源
23 pthread_exit(NULL);
24 }
25
26 void *fun3(void *arg)
27 {
28 sem_wait(&sem3);//因sem3在初始化时无资源会被阻塞,直至22行代码执行 不被阻塞 sem3-1 下次会阻塞
29 printf("%s:Pthread Come!\n",__FUNCTION__);
30 sem_post(&sem1);// 使得sem1获取到资源
31 pthread_exit(NULL);
32 }
33
34 int main()
35 {
36 int ret;
37 pthread_t tid1,tid2,tid3;
38 ret = sem_init(&sem1,0,1); //初始化信号量1 并且赋予其资源
39 if(ret < 0){
40 perror("sem_init");
41 return -1;
42 }
43 ret = sem_init(&sem2,0,0); //初始化信号量2 让其阻塞
44 if(ret < 0){
45 perror("sem_init");
46 return -1;
47 }
48 ret = sem_init(&sem3,0,0); //初始化信号3 让其阻塞
49 if(ret < 0){
50 perror("sem_init");
51 return -1;
52 }
53 ret = pthread_create(&tid1,NULL,fun1,NULL);//创建线程1
54 if(ret != 0){
55 perror("pthread_create");
56 return -1;
57 }
58 ret = pthread_create(&tid2,NULL,fun2,NULL);//创建线程2
59 if(ret != 0){
60 perror("pthread_create");
61 return -1;
62 }
63 ret = pthread_create(&tid3,NULL,fun3,NULL);//创建线程3
64 if(ret != 0){
65 perror("pthread_create");
66 return -1;
67 }
68 /*回收线程资源*/
69 pthread_join(tid1,NULL);
70 pthread_join(tid2,NULL);
71 pthread_join(tid3,NULL);
72
73 /*销毁信号量*/
74 sem_destroy(&sem1);
75 sem_destroy(&sem2);
76 sem_destroy(&sem3);
77
78 return 0;
79 }
80
运行结果:
该例程加入了信号量的控制使得线程的执行顺序变为可控的,在初始化信号量时,将信号量1填入资源,使之不被sem_wait函数阻塞,在执行完逻辑后使用sem_pos函数来填入即将要执行的资源。当执行函数sem_wait后,会执行sem自减操作,使下一次竞争被阻塞,直至通过sem_pos被释放。
上述例程因38行初始化信号量1时候,使其默认获取到资源,43、48行初始化信号量2、3时候,使之没有资源。于是在线程处理函数中,每个线程通过sem_wait函数来等待资源,发送阻塞现象。因信号量1初始值为有资源,故可以先执行线程1的逻辑。待执行完第12行sem_wait函数,会导致sem1-1,使得下一次此线程会被阻塞。继而执行至14行,通过sem_post函数使sem2信号量获取资源,从而冲破阻塞执行线程2的逻辑...以此类推完成线程的有序控制。
5.3 总结
有关多线程的创建流程下图所示,首先需要创建线程,一旦线程创建完成后,线程与线程之间会发生竞争执行,抢占时间片来执行线程逻辑。在创建线程时候,可以通过创建线程的第四个参数传入参数,在线程退出时亦可传出参数被线程回收函数所回收,获取到传出的参数。
线程编程流程
当多个线程出现后,会遇到同时操作临界公共资源的问题,当线程操作公共资源时需要对线程进行保护加锁,防止其与线程在此线程更改变量时同时更改变量,待逻辑执行完毕后再次解锁,使其余线程再度开始竞争。互斥锁创建流程下图所示。
互斥锁编程流程
当多个线程出现后,同时会遇到无序执行的问题。有时候需要对线程的执行顺序做出限定,变引入了信号量,通过PV操作来控制线程的执行顺序,下图所示。
信号量编程流程