Linux嵌入式文件IO一文全部搞定【6万字爆肝文学】

嵌入式文件IO一文全部搞定

第一章 《嵌入式文件io基本概念》

【1】★文件IO的概念★

在Linux系统语境下,文件(file)一般有两个基本含义:

狭义:指普通的文本文件,或二进制文件。包括日常所见的源代码、word文档、压缩包、图片、视频文件等等。

在这里插入图片描述

广义:除了狭义上的文件外,几乎所有可操作的设备或接口都可视为文件。包括键盘、鼠标、硬盘、串口、触摸屏、显示器等,也包括网络通讯端口、进程间通讯管道等抽象概念。
在这里插入图片描述

【2】★Linux系统中文件IO的分类★

在Linux中,文件总共被分成了7种,他们分别是:

普通文件:存在于外部存储器中,用于存储普通数据。 目录文件:用于存放目录项,是文件系统管理的重要文件类型。
管道文件:一种用于进程间通信的特殊文件,也称为命名管道FIFO。 套接字文件:一种用于网络间通信的特殊文件。
链接文件:用于间接访问另外一个目标文件,相当于Windows快捷方式。 字符设备文件:字符设备在应用层的访问接口。
块设备文件:块设备在应用层的访问接口。

gec@ubuntu:~$ ls -l
-rw-r--r-- 1 gec gec  345  Sep  12:38 a.zip
drwxr-xr-x 2 gec gec 1024  Sep  12:38 dir/
prw-r--r-- 1 gec gec    0  Sep  12:38 pipe 
srw-r--r-- 1 gec gec    0  Sep  12:38 socket
lrw-r--r-- 1 gec gec    4  Sep  12:38 link -> a.zip
crw-r--r-- 1 gec gec 1, 3  Sep  12:38 character
brw-r--r-- 1 gec gec 5, 1  Sep  12:38 block

注意到,每个文件信息的最左边一栏,是各种文件类型的缩写,从上到下依次是:

-(regular)普通文件
d(directory)目录文件
p(pipe)管道文件(命名管道)
s(socket)套接字文件(Unix域/本地域套接字)
l(link)链接文件(软链接)
c(character)字符设备文件
b(block)块设备文件

【3】★系统IO与标准IO★

对文件的操作,基本上就是输入输出,因此也一般称为IO接口。在操作系统的层面上,这一组专门针对文件的IO接口就被称为系统IO;在标准库的层面上,这一组专门针对文件的IO接口就被称为标准IO,如下图所示:

在这里插入图片描述

系统IO:是众多系统调用当中专用于文件操作的一部分接口。
标准IO:是众多标准函数当中专用于文件操作的一部分接口。

从图中还能看到,标准IO实际上是对系统IO的封装,系统IO是更接近底层的接口。如果把系统IO比喻为菜市场,提供各式肉蛋菜果蔬,那么标准IO就是对这些基本原来的进一步封装,是品类和功能更加丰富的各类酒庄饭店。

【4】★如何选择系统IO与标准IO★

系统IO

由操作系统直接提供的函数接口,特点是简洁,功能单一 没有提供缓冲区,因此对海量数据的操作效率较低
套接字Socket、设备文件的访问只能使用系统IO

标准IO

由标准C库提供的函数接口,特点是功能丰富 有提供缓冲区,因此对含量数据的操作效 率高 编程开发中尽量选择标准IO,但许多场合只能用系统IO
总的来讲,这两组函数接口在实际编程开发中都经常会用到,都是基本开发技能。

第二章 《嵌入式系统IO基本API》

【5】★文件的打开Open与关闭Close★

打开文件
在这里插入图片描述
关键点:

open函数有两个版本,一个有两个参数,一个有三个参数。 当打开一个已存在的文件时 ,指定两个参数即可。
当创建一个新文件时,需要用第三个参数指定新文件的权限,否则新文件的权限是随机值。 模式flags,可以使用位或的方式,来同时指定多个模式。
模式flags中,O_NOCTTY主要用在后台精灵进程,阻止这些精灵进程拥有控制终端。

示例代码:

int main(void)
{
    
    
    int fd;

    // 以下三种打开方式,都要求文件已存在,否则失败返回
    fd = open("a.txt", O_RDWR);   // 以可读可写方式打开
    fd = open("a.txt", O_RDONLY); // 以只读方式打开
    fd = open("a.txt", O_WRONLY); // 以只写方式打开

    // 1. 如果文件不存在,则创建该文件,并设置其权限为0644
    // 2. 如果文件已存在,则失败返回
    fd = open("a.txt", O_RDWR|O_CREAT|O_EXCL, 0644);   // 以可读可写方式打开
    fd = open("a.txt", O_RDONLY|O_CREAT|O_EXCL, 0644); // 以只读方式打开
    fd = open("a.txt", O_WRONLY|O_CREAT|O_EXCL, 0644); // 以只写方式打开

    // 1. 如果文件不存在,则创建该文件,并设置其权限为0644
    // 2. 如果文件已存在,则清空该文件的原有内容
    fd = open("a.txt", O_RDWR|O_CREAT|O_TRUNC, 0644);   // 以可读可写方式打开
    fd = open("a.txt", O_RDONLY|O_CREAT|O_TRUNC, 0644); // 以只读方式打开
    fd = open("a.txt", O_WRONLY|O_CREAT|O_TRUNC, 0644); // 以只写方式打开

    // 以下三种打开方式,都要求文件已存在,否则失败返回
    fd = open("a.txt", O_RDWR|O_APPEND, 0644);   // 以可读可写方式追加文件内容
    fd = open("a.txt", O_WRONLY|O_APPEND, 0644); // 以只写方式追加文件内容
}

关闭文件
在这里插入图片描述
关键点

当不再使用一个文件时,应当关闭该文件,防止系统资源浪费。 对同一文件重复执行关闭操作会失败返回,不会有其他副作用。

【6】★标准库函数的错误处理★

在所有的库函数中,如果调用过程出错了,那么该函数除了会返回一个特定的数据来告诉用户调用失效之外,还都会去修改一个大家共同的全局错误码errno,我们可以通过这个错误码,来进一步确认究竟是什么错误。

例如:

#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h> // 全局错误码声明所在的头文件
int main()
{
    
    
    int fd = open("a.txt", O_RDWR);
    if(fd == -1)
    {
    
    
        // 以下两条语句效果完全一致:输出函数出错的原因
        perror("打开a.txt失败");
        printf("打开a.txt失败:%s\n", strerror(errno));
    }
    return 0;
}

关键点:

如果库函数、系统调用出错了,全局错误码 errno 会随之改变 如果库函数、系统调用没出错,全局错误码 errno 不会改变
一个库函数、系统调用出错后,若未及时处理错误码,则错误码可能会被随后的其他函数修改

提取错误码信息的两种办法:

// 1. 使用perror(),直接输出用户信息和错误信息:
if(open("a.txt", O_RDWR) == -1)
{
    
    
    perror("打开a.txt失败");
}

// 2. 使用strerror(),返回错误信息交给用户自行处理:
if(open("a.txt", O_RDWR) == -1)
{
    
    
    printf("打开a.txt失败:%s\n", strerror(errno));
}

以上代码输出的结果是完全一样的:

gec@ubuntu:~$ ./a.out
打开a.txt失败: No such file or directory
打开a.txt失败: No such file or directory

一般而言,perror()用起来更加方便,但有时候需要使用strerror()来输出一些更加灵活的信息,比如以上代码,如果打开文件的名字不是固定a.txt,而是取决于外部参数,那么就应该写错:

if(open("a.txt", O_RDWR) == -1)
{
    
    
    printf("打开%s失败:%s\n", argv[1], strerror(errno));
}

以上代码输出的结果是:

gec@ubuntu:~$ ./a.out a.txt
打开a.txt失败: No such file or directory
gec@ubuntu:~$ 
gec@ubuntu:~$ ./a.out ELF-V4.tar.gz
打开ELF-V4.tar.gz失败: No such file or directory
gec@ubuntu:~$ 

【7】★文件描述符本质★

函数 open() 的返回值,是一个整型 int 数据。这个整型数据,实际上是内核中的一个称为 fd_array 的数组的下标:

在这里插入图片描述

打开文件时,内核产生一个指向 file{} 的指针,并将该指针放入一个位于 file_struct{} 中的数组 fd_array[] 中,而该指针所在数组的下标,就被 open() 返回给用户,用户把这个数组下标称为文件描述符,如上图所示。

结论:

文件描述符从0开始,每打开一个文件,就产生一个新的文件描述符。
可以重复打开同一个文件,每次打开文件都会使内核产生系列结构体,并得到不同的文件描述符 由于系统在每个进程开始运行时,都默认打开了一次键盘、两次屏幕,因此0、1、2描述符分别代表标准输入、标准输出和标准出错三个文件(两个硬件)。

在这里插入图片描述

【8】★文件的读read写write操作★

在这里插入图片描述
关键点:

参数count是读写字节数的愿望值,实际读写成功的字节数由返回值决定。 读取普通文件时,如果当读到了文件尾,read()会返回0。
读取管道文件时,如果管道中没有数据,read()默认会阻塞。

读取文件内容:

// 1. 将文件 a.txt 中的内容读出来,并显示到屏幕上
int fd = open("a.txt", O_RDWR);

char buf[100];
int n;
while(1)
{
    
    
    bzero(buf, 100);
    n = read(fd, buf, 100); // 每次最多读取100个字节

    if(n == 0) // 读完退出
        break;

    printf("%s", buf);
}
close(fd);

写入文件内容:

// 2. 将键盘输入的内容,写入文件 a.txt
int fd = open("a.txt", O_RDWR);

char buf[100];
bzero(buf, 100);

// 从键盘输入数据
fgets(buf, 100, stdin);

// 将输入的数据写入文件
write(fd, buf, strlen(buf));
close(fd);

【9】★文件的读read写write位置★

当我们对文件进行读写操作时,系统会为我们记录操作的位置,以便于下次继续进行读写操作的时候,从适当的地方开始。

有几点需要注意:

每当open一个文件,系统就会维护一套包括文件操作位置在内的相关信息。
对同一个文件描述符进行读写操作时,使用的同一套文件信息,影响的是同一个位置参数。
对同一个文件的多个不同的文件描述符进行读写操作时,使用的是不同的文件信息,影响的是不同的位置参数,彼此互相之间独立,这往往会导致文件信息的错乱。

示例代码1:读写作用于同一个文件位置

#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <strings.h>

int main(int argc, char **argv) // ./main a.txt
{
    
    
    int fd = open(argv[1], O_RDWR);
    char buf[100];

    // 读出1个字节,读完后读写操作位置是第2个字节
    bzero(buf, 100);
    read(fd, buf, 1);
    printf("%s\n", buf);

    // 写入2个字节,写完后读写操作位置是第4个字节
    bzero(buf, 100);
    write(fd, "xx", 2);

    // 读出3个字节,读完后读写操作位置是第7个字节
    bzero(buf, 100);
    read(fd, buf, 3);
    printf("%s\n", buf);

    return 0;
}

示例代码2:多次打开得到不同的文件描述符,各自读写操作位置独立

#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <strings.h>

int main(int argc, char **argv) // ./main a.txt
{
    
    
    // 假设文件中的原始内容是:abcdefghijk
    int fd1 = open(argv[1], O_RDWR);
    int fd2 = open(argv[1], O_RDWR);
    char buf[100];

    // 读出1个字节,读完后读写操作位置是第2个字节
    // 此时影响的是fd1,对fd2没有影响
    bzero(buf, 100);
    read(fd1, buf, 1);
    printf("%s\n", buf); // 输出a

    // 写入2个字节,读完后读写操作位置是第3个字节
    // 与上述读操作没有关系
    bzero(buf, 100);
    write(fd2, "xy", 2); // ab被覆盖,原文件变成xycdefghijk

    // 读出3个字节,读完后读写操作位置是第5个字节
    // 此时影响的是fd1,对fd2没有影响
    bzero(buf, 100);
    read(fd1, buf, 3);
    printf("%s\n", buf); // 输出ycd

    return 0;
}

【10】★读写位置lseek的设置★

对文件进行常规的读写操作的时候,系统会自动调整读写位置,以便于让我们顺利地顺序读写文件,但如果有需要,文件的读写位置是可以任意调整的,调整函数接口如下:

在这里插入图片描述
关键点:

1.lseek函数可以将文件位置调整到任意的位置,可以是已有数据的地方,也可以是未有数据的地方,假设调整到文件末尾之后的某个地方,那么文件将会形成所谓“空洞”。
2.lseek函数只能对普通文件调整文件位置,不能对管道文件调整。
3.lseek函数的返回值是调整后的文件位置距离文件开头的偏移量,单位是字节。

示例代码:

int main(void)
{
    
    
    // 假设文件 a.txt 只有一行
    // 内容如下:
    //
    // 1234567890abcde 
    //

    int fd = open("a.txt", O_RDWR);

    // 读取前面10个阿拉伯数字:
    char buf[10];
    read(fd, buf, 10);

    // 将文件位置调整到'c'
    lseek(fd, 2, SEEK_CUR);

    // 将文件位置调整到'1'
    lseek(fd, 0, SEEK_SET);

    // 将文件位置调整到'a'
    lseek(fd, -5, SEEK_END);
}

第三章 《嵌入式系统IO常用API》

【11】★系统IO常用API 概述★

对文件的操作,除了最基本的打开、关闭、读、写、定位之外,还有很多特殊的情况,比如用于沟通应用层与底层驱动之间的ioctl、万能工具箱fcntl、内存映射mmap等等,熟练使用这些API,是日常开发的必备技能。

在这里插入图片描述

【12】★系统IO常用API之ioctl()

该函数是沟通应用层和驱动层的有力武器,底层开发人员在为硬件设备编写驱动的时候,常常将某些操作封装为一个函数,并为这些接口提供一个所谓的命令字,应用层开发者可以通过 ioctl() 函数配合命令字,非常迅捷地绕过操作系统中间层层机构直达驱动层,调用对应的功能。

从这个意义上讲,函数 ioctl() 像是一个通道,只提供函数调用路径,具体的功能由所谓命令字决定,下面是函数的接口规范说明:

在这里插入图片描述
关键点:

request 就是所谓的命令字。 底层驱动开发者可以自定义命令字。 对于某些常见的硬件设备的常见功能,系统提供了规范的命令字。

示例代码:

int main(void)
{
    
    
    // 打开一盏LED灯
    int led = open("/dev/Led", O_RDWR);

    // 通过命令字 LEDOP 及其携带的0/1参数,控制LED灯的亮灭
    // 此处,LEDOP 是底层开发者自定义的命令字
    ioctl(led, LEDOP, 1);
    ioctl(led, LEDOP, 0);

    // 打开一个摄像头
    int cam = open("/dev/video0", O_RDWR);

    // 通过命令字 VIDIC_STREAMON 及其携带参数 vtype 启动摄像头
    enum v4l2_buf_type vtype= V4L2_BUF_TYPE_VIDEO_CAPTURE;
    ioctl(cam, VIDIOC_STREAMON, &vtype);
}

【13】★系统IO常用API之dup() 与 dup2()★

dup 是英语单词 duplicate 的缩写,意即“复制”。

这两个函数功能类似,都是用来“复制”文件描述符,接口规范如下:
在这里插入图片描述

请注意,文件描述符只是一个整数,这两个函数的关键点不在于复制这两个整数,而是复制它们所对应的在内核中的一套资源,并让一个新的文件描述符去指代这套系统资源。

示例代码:

int main()
{
    
    
    // 打开文件 a.txt ,获得其文件描述符 fd1
    // 此处 fd1 就代表了这个文件及其配套的系统资源
    int fd1 = open("a.txt", O_RDWR);

    // 复制文件描述符 fd1,默认得到最小未用的文件描述符
    dup(fd1);

    // 复制文件描述符 fd1,并指派为 100
    dup2(fd1, 100);
}

解析:

使用dup函数时,会自动分配当前未用的最小的文件描述符,如上述代码,由于进程默认打开了0、1、2作为标准输入输出,于是 fd1就是3,新产生的文件描述符就是4,而 dup2 函数可以任意指定文件描述符的数值,如果指定的文件描述符已经有所指代,那么原先指代关系将会被替换。这种情况被称为“重定向”。
在这里插入图片描述

【14】★系统IO常用API之fcntl()★

该函数的名字是 file control 的缩写,顾名思义,它可以用来“控制”文件,与 ioctl 类似,此处的 “控制” 含义广泛,具体内容由其第二个参数命令字来决定,fcntl 的接口规范如下:
在这里插入图片描述

关键点:

fcntl 是个变参函数,前两个参数是固定的,后续的参数个数和类型取决于 cmd 的具体数值。 第二个参数 cmd,称为命令字。
命令字有很多,常用的如下:
在这里插入图片描述

从上表可以看出:

  1. F_DUPFD的功能与dup( )/dup2( )类似。
  2. 通过F_SETSL/F_GETFL来获取和设置文件status,经常拿来设置文件的阻塞特性。
  3. 通过F_SETOWN/F_GETOWN来获取和设置套接字触发的信号的属主,网络编程中常见。

示例代码①:

// 获取指定文件fd的标签属性
int flag = fcntl(fd, F_GETFL);

// 在其原有属性上,增添非阻塞属性
flag |= O_NONBLOCK;
fcntl(fd, F_SETFL, flag);

示例代码②:

// 将套接字sockfd的信号属主设置为本进程
fcntl(sockfd, F_SETOWN, getpid());

在网络编程中,当一个套接字处于异步通信状态并收到一个远端的数据时,就会使得内核产生一个信号SIGIO,此时我们可以通过上述fcntl()技巧告诉内核这个信号的接收者。一般而言,接收者收到该信号后,就知道套接字上有数据等待处理了。

【15】★系统IO常用API之mmap()★

该函数全称是 memory map,意为内存映射,即将某个文件与某块内存关联起来,达到通过操作这块内存来间接操作其所对应的文件的效果。

在这里插入图片描述
关键点:

mmap函数的flags参数是有很多的,上表只罗列了最简单的几个,详细信息请使用 man 手册进行查阅。
mmap函数理论上可以对任意文件进行内存映射,但通常用来映射一些比较特别的设备文件,比如液晶屏LCD。

注意:

在较旧的Linux内核(如2.6内核)中,可以直接使用mmap()来给LCD设备映射内存,但在较新Linux内核(如4.4内核)中,则需要经由DRM统一管理,不可直接mmap映射显存。

示例代码1,映射普通文件:
hh## 标题

int main()
{
    
    
    // 以只读方式打开一个文件
    int fd = open("a.txt", O_RDWR);

    // 申请一块大小为1024字节的映射内存,并将之与文件fd相关联
    char *p = mmap(NULL, 1024, PROT_READ|PROT_WRITE,
                MAP_SHARED, fd, 0);

    // 将该映射内存的内容打印出来(即其相关联文件fd的内容)
    printf("%s\n", p);

    // 通过操作内存,间接修改了文件内容
    p[0] = 'x';
    printf("%s\n", p);
    
    // 解除映射
    munmap(p, 1024);
    return 0;
}

注意几点:

1.使用 mmap 映射内存时,指定的读写权限必须是打开模式的子集。
2.映射模式为 MAP_SHARED 时,修改的内容会影响文件。
3.映射模式为 MAP_PRIVATE 时,修改的内容不会影响文件。

示例代码2,映射液晶屏LCD文件(注意:只适用于GEC6818及之前开发板):

int main()
{
    
    
    // 打开液晶屏文件
    int lcd = open("/dev/fb0", O_RDWR);

    // 给LCD设备映射一块内存(或称显存)
    char *p = mmap(NULL, 800*480*4, PROT_WRITE,
                   MAP_SHARED, lcd, 0);

    // 通过映射内存,将LCD屏幕的每一个像素点涂成红色
    int red = 0x00FF0000;
    for(int i=0; i<800*480; i++)
        memcpy(p+i*4, &red, 4);

    // 解除映射
    munmap(p, 800*480*4);
    return 0;
}

在这里插入图片描述
二进制文件和文本文件有什么区别?

答:
首先,任何文件在存储的时候而是以0/1序列存储的,也就是说计算机中的任何数据(包括任何类型的文件),实际存储形式都是二进制的。
其次,我们平常说的二进制文件和文本文件,指的不是他们的实际存储形式,而是指这些数据是否是某一种字符编码,比如ASCII码、中文的GB2312编码、UTF编码等等,按此类编码存储的数据,可以被各种编辑器解析并呈现出来。
如果某文件的内容不是按照某种字符编码存储,那么编辑器打开的时候就会呈现乱码,因为编译器一定是按照某种字符编码来解释这些数据的。

【16】★系统IO常用API之本章实例★

第一题: 使用已学过的系统IO接口,测试默认情况下进程最多可以打开多少个文件。并在无法打开更多文件的情况下,将出错信息妥善输出。

提示:同一个文件可以以不同模式被多次打开。
示例代码:

#include <stdio.h> 
#include <fcntl.h> 
int main(void)
{
    
    
    int n = 0;
    while(1)
    {
    
    
        int fd = open("a.txt", O_RDWR);

        if(fd > 0)
            n++;

        else
        {
    
    
            perror("open()失败");
            break;
        }
    }
    printf("n:%d\n", n);
}
执行结果:

gec@ubuntu:$ ./a.out
open()失败: Too many open files
n:1021

其中,n 为1021意味着系统默认情况下可以允许一个进程同时打开1024个文件,还有3个是起始状态下已经被默认打开了的文件,它们是标准输入设备、标准输出设备和标准出错设备。

第二题:编写一个程序,通过计算读取数据的数量,获取指定文件的大小。

示例代码:

#include <stdio.h> 
#include <stdlib.h> 
#include <fcntl.h> 
#include <unistd.h> 

int main(int argc, char **argv)
{
    
    
    int fd = open(argv[1], O_RDONLY);
    if(fd == -1)
    {
    
    
        perror("open失败");
        exit(0);
    }

    int total = 0; // 记录文件字节数

    char ch;
    while(1)
    {
    
    
        int n = read(fd, &ch, 1);
        if(n == 0)
        {
    
    
            printf("读取完毕\n");
            break;
        }
        else if(n == -1)
        {
    
    
            perror("read失败");
            exit(0);
        }

        total++;
    }

    printf("文件大小:%d\n", total);
}
执行结果:

gec@ubuntu:$ ./a.out someFile
读取完毕
文件大小:37686

第三题:根据对文件位置调整的认识,将一个文件的读写位置调整到末尾10k处,然后写入一些数据,比较该文件的实际大小和显示大小,体会“文件空洞”的概念。

示例代码:

#include <stdio.h> 
#include <stdlib.h> 
#include <fcntl.h> 
#include <unistd.h> 

int main(void)
{
    
    
    int fd = open("fileWithHole", O_CREAT|O_TRUNC|O_WRONLY);
    if(fd == -1)
    {
    
    
        perror("open失败");
        exit(0);
    }

    write(fd, "abc", 3);
    lseek(fd, 10*1024, SEEK_SET);
    write(fd, "xyz", 3);

    close(fd);

    return 0;
}
执行结果是:

gec@ubuntu:$ ./a.out
gec@ubuntu:$ ls -l
-rw-r-x--- 1 501 dialout  10243 415 19:06 fileWithHole

【1】编写一个程序,用系统IO提供的函数接口,实现文件的拷贝。
在这里插入图片描述
参考代码:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>

#define BLKSIZE 200

int main(int argc, char **argv)
{
    
    
	int fd_from, fd_to;

	if(argc != 3)
	{
    
    
		printf("对不起,您必须指定两个文件\n");
		printf("像这样:./copyFile file1 file2\n");
		exit(0);
	}
	
	// 打开源文件
	fd_from = open(argv[1], O_RDONLY));
	if(fd_from == -1)
	{
    
    
		printf("打开文件“%s”失败:%s\n", argv[1], strerror(errno));
		exit(0);
	}

	// 打开目标文件
	fd_to = open(argv[2], O_WRONLY|O_CREAT|O_TRUNC, 0644);
	if(fd_to == -1)
	{
    
    
		printf("打开文件“%s”失败:%s\n", argv[2], strerror(errno));
		exit(0);
	}

	char buf[BLKSIZE];
	char *bp;
	int nread, nwrite;

	// 循环将数据从文件argv[1]复制到argv[2]中
	while(1)
	{
    
    
		// 读取源文件内容,若被信号中断,则重启读操作
		while(((nread=read(fd_from, buf, BLKSIZE)) == -1) 
                && (errno == EINTR));

		// 若遇到错误,则报告错误信息并退出
		if(nread == -1)
		{
    
    
			perror("读取源文件失败");
			break;
		}

		// 若读到文件尾,则退出
		if(nread == 0)
			break;

		// 循环将读取到的 nread 个字节写入目标文件中
		bp = buf;
		while(nread > 0)
		{
    
    
			while(((nwrite=write(fd_to, bp, nread)) == -1)
                    && (errno == EINTR));

			if(nwrite <= 0)
			{
    
    
				perror("写入失败");
				exit(0);
			}

			nread -= nwrite;
			bp += nwrite;
		}
	}

	close(fd_from);
	close(fd_to);

	return 0;
}

(标准IO缓冲区)

【2】编写一个程序,检测标准IO的缓冲区的大小。

解析:

普通文件默认是全缓冲,可以利用这个特性,一个字节一个字节写入数据,并且在每次写入数据之后获取文件的大小,直到把缓冲区填满即可测得缓冲区的大小。

参考代码:

#include <time.h>
#include <errno.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <string.h>
#include <strings.h>
#include <stdbool.h>
#include <pthread.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <sys/types.h>

int main(int argc, char **argv)
{
    
    
	// 结构体stat可以用来存放文件的信息,包括大小
	struct stat info;
	bzero(&info, sizeof(info));

	// 打开文件
	FILE *fp = fopen("a.txt", "w");

	while(1)
	{
    
    
		// 每次写入1个字节
		fwrite("a", 1, 1, fp);

		// 函数stat可以用来获取文件的信息,包括大小
		stat("a.txt", &info);

		if(info.st_size > 0)
			break;
	}

	printf("缓冲区的大小: %ld\n", info.st_size);

	fclose(fp);
	return 0;
}

(系统时间、标准IO、标准IO缓冲区处理)

【3】编写一个程序,使之每隔1秒保存一次当前系统的时间和行号,保存到一个普通文件 time.txt中。要求在按 ctrl+c
终止该程序后,文件 time.txt 能正常保存到所有的信息,并且在重启程序能继续保存时间,并且保持行号连续,如下所示。

gec@ubuntu:~$ ./a.out
 1, Sat Jan  5 02:24:02 2020
 2, Sat Jan  5 02:24:03 2020
 3, Sat Jan  5 02:24:04 2020
^C
gec@ubuntu:~$ ./a.out
 4, Sat Jan  5 02:26:21 2020
 5, Sat Jan  5 02:26:22 2020

解析:

本题关键在于,循环输出的情形下,要让程序及时冲洗数据否则数据会丢失。将全缓冲类型的数据冲洗到内核有很多办法,可以采用最简单fflush强制冲洗。
本题另一个考点在于,如何在系统时间前面加上需要,这只需复习一下snprintf函数即可迎刃而解。

参考代码:

#include <stdio.h>
#include <sys/stat.h>
#include <time.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>

#define BUFSIZE 64

static int line_num = 0;

FILE *Fopen(const char *path, const char *mode)
{
    
    
	FILE *fp;
	fp = fopen(path, mode);

	if(fp == NULL)
		perror("fopen error");

	return fp;
}

int main(int argc, char **argv)
{
    
    
	FILE *fp = NULL;
	FILE *fpline = NULL;

	if(access("line", F_OK))
		fpline = Fopen("line", "w+");

	else{
    
    
		/*
		** open the file "line" without 
		** overwrite its content.
		*/
		fpline = Fopen("line", "r");

		fread(&line_num, sizeof(int), 1, fpline);

		// open the file "line" with mode "w+"
		freopen("line", "w+", fpline);
	}
	setvbuf(fpline, NULL, _IONBF, 0);


	/*
	** fopen the file "times2" with mode "a+"
	** then it can be created when it is not
	** exist and won't be overwrited when it
	** does exist.
	*/
	fp = Fopen("times", "a+");

	setvbuf(fp, NULL, _IONBF, 0);

	char num_str[BUFSIZE]; 

	time_t *tloc=(time_t *)malloc(sizeof(time_t));


	// obtain system time every 1s in this non-ending loop
	while(1){
    
    

		snprintf(num_str, BUFSIZE, "%d\t", ++line_num);

		time(tloc);
		snprintf(num_str+strlen(num_str)-1, BUFSIZE, "\t%s", ctime(tloc));
		fputs(num_str, fp);

		rewind(fpline);
		fwrite(&line_num, sizeof(int), 1, fpline);


		fputs(num_str, stdout);

		sleep(1);
	}

	return 0;
}

第四章 《文件操作和目录属性》

在应用开发中,经常要获取文件的属性,例如:文件的类型、大小、权限、设备号、最近修改时间等等,比如网络传输文件时,一般都需要先传递文件的属性,等准备妥善了再开始传输文件的真实内容。因此,熟悉文件属性的细节,并熟练获取这些信息的方式至关重要。


【17】文件属性

1. 获取文件属性信息的函数接口

如下函数可以获取指定文件的属性:
在这里插入图片描述

注意:
以上三个函数的功能一样,区别如下:

`stat针对文件名获取其信息。
fstat针对文件描述符获取其信息。
lstat可以获取软连接文件本身的属性。`

这几个函数获取了文件的属性之后,会将这些信息存入一个称为stat的结构体中(与函数同名),该结构体的细节如下:

struct stat
{
    
    
     dev_t     st_dev;    // 本文件所在的设备的设备号,适用于非设备文件
     ino_t     st_ino;    // i节点号,相当于身份证号码
     mode_t    st_mode;   // 文件类型 + 文件权限
     nlink_t   st_nlink;  // 文件的别名的数目
     uid_t     st_uid;    // 文件所有者ID
     gid_t     st_gid;    // 文件所在组ID
     dev_t     st_rdev;   // 本文件的设备号,适用于特殊设备文件   
     off_t     st_size;   // 文件大小
     blksize_t st_blksize;   
     blkcnt_t  st_blocks;   
 
     // 文件时间戳
     struct timespec st_atim;  // 最近访问时间,比如打开看一下文件的时间
     struct timespec st_mtim;  // 最近修改时间,比如打开并改一下的时间
     struct timespec st_ctim;  // 最近状态改变时间,比如修改了文件的权限的时间
 };
 
// 其中,时间戳结构体的细节是:
struct timespec
{
    
    
     long    tv_sec;   /* 秒 */
     long    tv_nsec;  /* 纳秒 */
};

下面对较复杂的成员做详细介绍:

2. 文件的设备号

struct stat
{
    
    
     dev_t     st_dev;    // 本文件所在的设备的设备号,适用于非设备文件
     ...
     dev_t     st_rdev;   // 本文件的设备号,适用于特殊设备文件
     ...
     ...
 };

Linux系统为了方便管理,为每一种设备分配了主次设备号,主设备号用来规范设备的类型;次设备号用来规范该种设备在本系统中的序号。设备号是系统资源,在设备被加载的时候分配完毕。

以下两个函数,常用来获取主次设备号:

unsigned int major(dev_t dev);  // 从 dev 中获取主设备号
 unsigned int minor(dev_t dev);  // 从 dev 中获取次设备号

注意,在结构体stat中有两个类型为 dev_t 的成员。他们分别代表:

`本文件所在设备的设备号,适用于非设备文件。
本文件的设备号,适用于特殊设备文件。
特殊设备文件只有设备号的属性,没有文件大小的属性,即st_size是无效的。`

解析:

非设备文件是没有设备号的,此时该文件的 st_rdev 成员是无效的。 非设备文件一定是存储某个存储器上的,此时该文件的 st_dev
代表的是其所在存储器的设备号,比如某个硬盘。 特殊设备文件指的是类型为字符设备或块设备的文件,比如键盘、鼠标、硬盘、显示器等。
特殊设备文件的 st_dev 成员是无效的,只有 st_rdev 有效。

示例代码:

int main(int argc, char **argv)
{
    
    
     // 获取指定文件的属性信息
     struct stat info;
     stat(argv[1], &info); 

     // 是特定的设备文件,那么 st_rdev 有效且 st_size 无效
     if(S_ISCHR(info.st_mode) || S_ISBLK(info.st_mode))
     {
    
    
          printf("该文件的主次设备号分别是:%d,%d\n",
                  major(info.st_rdev),
                  minor(info.st_rdev));
     }

     // 不是设备文件,那么 st_dev 有效且 st_size 也有效
     else
     {
    
    
          printf("文件大小是:%d", info.st_size);
          printf("文件所在设备的主次设备号分别是:%d,%d\n",
                  major(info.st_dev),
                  minor(info.st_dev));
     }
}

3. 类型与权限

struct stat
{
    
    
     mode_t    st_mode;   // 文件类型 + 文件权 
     ...
     ...
 };

在结构体 stat 中,文件的类型和权限并没有分开存储,而是被统一存储到同一个成员 st_mode 中,该成员的内部结构如下所示:
在这里插入图片描述

st_mode内部结构
关键点:

st_mode是一个16位的 short 短整型数据。 前4位表达文件的类型,由于Linux文件类型总共7种,足够。 中间三位分别是
setuid、setgid 和 stickyBit。 后9位表达文件的权限,与三组权限一一对应。 判断文件的类型可以用如下宏即可:

S_ISREG(st_mode)  is it a regular file?
S_ISDIR(st_mode)  directory?
S_ISCHR(st_mode)  character device?
S_ISBLK(st_mode)  block device?
S_ISFIFO(st_mode) FIFO (named pipe)?
S_ISLNK(st_mode)  symbolic link?  (Not in POSIX.1-1996.)
S_ISSOCK(st_mode) socket?  (Not in POSIX.1-1996.)

setuid、setgid(只对普通文件有效) 与 stickyBit(只针对目录有效) 作用解析:

`setuid:使得文件的使用者获得文件所有者的临时授权。
setgid:使得文件的使用者获得文件所在目录的所属组的临时授权。
stickyBit:使得用户只能增加和删除属于自身的文件,不能删除别的用户的文件。`

修改文件setID示例:

gec@ubuntu:~$ ls -l
-rwxr-xr-x 1 gec gec  8520 Dec 18 00:27 a.out
gec@ubuntu:~$
gec@ubuntu:~$ chmod 04755 a.out
gec@ubuntu:~$ ls -l
-rwsr-xr-x 1 gec gec  8520 Dec 18 00:27 a.out

以上示例中,通过 04755 给程序 a.out 添加了
setuid,这意味着:以后不管哪个用户来执行该程序,其执行期间都会获得该程序所有者 gec 的临时授权,简单讲就是会临时变成用户 gec:


如果程序 a.out 创建了新文件,那么这些新文件将属于 gec。 如果程序 a.out 触发了权限检查,那么系统会检查 gec 的权限。
同样的道理,适用于 setgid。

使用 setID 解决实际问题的一个典型例子,就是系统中用于修改密码的命令:

gec@ubuntu:~$ ls -l /usr/bin/passwd 
-rwsr-xr-x 1 root root 59640 Mar 23  2019 /usr/bin/passwd

程序命令 passwd 是属于根用户 root 的,但由于它被设置了setuid,因此以后不管是谁来执行这个程序,在其执行期间都会临时获得
root 的临时授权,这么做是因为修改密码的本质上修改文件 /etc/passwd 的内容,而该文件只有管理员 root 才能修改,设置了
setuid 之后,普通用户既可以通过命令 passwd 来修改此文件,也避免了索取管理员密码的步骤,非常实用。

修改目录stickyBit示例:

gec@ubuntu:~$ ls -l
drwxrwxrwx  2 gec gec 4096 Dec 20 18:13 dir/
gec@ubuntu:~$
gec@ubuntu:~$ chmod 01777 dir/
gec@ubuntu:~$ ls -l
drwxrwxrwt  2 gec gec 4096 Dec 20 18:13 dir/

目录 dir/ 的权限是 0777,表示任何用户都可以在该目录下可以增删任意的文件,但加了 stickyBit
限制之后,某用户只能删除自己创建的文件,而不能删除别的用户创建的文件,这个过程仿佛是自己创建的文件才“黏住”自己一样,因此这个功能被称为黏住位(stickyBit)

在 st_mode 中的末9位是文件的权限信息,标准IO库定义了如下宏来方便我们的操作:

S_IRWXU 00700,二进制:111 000 000(本权限位掩码)
S_IRUSR 00400,二进制:100 000 000
S_IWUSR 00200,二进制:010 000 000
S_IXUSR 00100,二进制:001 000 000

S_IRWXG 00070,二进制:000 111 000(本权限位掩码)
S_IRGRP 00040,二进制:000 100 000
S_IWGRP 00020,二进制:000 010 000
S_IXGRP 00010,二进制:000 001 000

S_IRWXO 00007,二进制:000 000 111(本权限位掩码)
S_IROTH 00004,二进制:000 000 100
S_IWOTH 00002,二进制:000 000 010
S_IXOTH 00001,二进制:000 000 001

示例代码:

int main(int argc, char **argv)
{
    
    
     // 获取指定文件的属性信息
     struct stat info;
     stat(argv[1], &info); 

     char mod[10];
     bzero(mod, 10);

     char rwx[3] = {
    
    'r', 'w', 'x'};
     for(int i=0; i<9; i++)
          mod[i] = info.st_mode&(0400>>i) ? rwx[i%3] : '-';

     printf("文件权限:%s\n", mod);
}

注意:

八进制数0400等价于二进制: 100 000 000

4. 其他简单文件信息
文件的所有者与所属组:

struct stat
{
    
    
     uid_t  st_uid;  // 文件所有者ID
     gid_t  st_gid;  // 文件所在组ID
     ...
};

从结构体 stat 中获取的文件所有者和所属组信息中,只有它们的ID号,而没有切确的名称,可以通过以下函数来获取切确的名称:

struct passwd *getpwuid(uid_t uid);
struct group *getgrgid(gid_t gid);

文件的尺寸相关信息:

struct stat
{
    
    
     nlink_t   st_nlink;   // 文件的别名的数目
     off_t     st_size;    // 文件大小
     blksize_t st_blksize; // 标准IO建议的数据块大小  
     blkcnt_t  st_blocks;  // 文件占用的数据块个数
     ...
};

关键点:

文件别名数目也称为硬链接数目,亦即索引个数,文件系统正是以此来判断某个文件是否可以彻底删除的标记。 文件大小就是执行命令 “ls -l”
时所展示大小,包含文件空洞的大小。

【18】目录操作

1. 基本概念

目录也是一种文件,因此操作流程与普通文件类似,有诸如打开、关闭、定位等概念,但目录是一种特殊的文件,目录存储的数据的最小单位并不是字符,而是目录项。这使得目录跟普通文件又有区别。

在Linux文件系统的经典结构中,目录不同于文件夹,目录的本质是索引,文件夹的本质是容器。在Linux中,目录有几个要点:


在这里插入图片描述
在这里插入图片描述

2. 创建、删除目录
在这里插入图片描述

在这里插入图片描述

示例代码:

int main(void)
{
    
    
    // 在家目录下创建一个空目录
    mkdir("/home/gec/a", 0755);

    // 将空目录删除(以下两条语句等价)
    rmdir("/home/gec/a");
    remove("/home/gec/a");
}

3. 打开、读取、关闭目录

与文件操作类似,要操作目录,首先是打开目录获取代表目录的“目录指针”,然后读取目录的基本单元“目录项”,最后关闭目录指针释放资源。操作函数如下:
在这里插入图片描述

关键点:

与文件指针类似,目录指针并不指向目录中的数据,它仅仅是代表了目录,可以作为后续操作的所谓句柄。
打开目录并不是进入目录,实际上进入目录的函数是:chdir()。 读取目录获得的不是一个个的字节,而是一个个“目录项”,如下所示:

// 目录项结构体
struct dirent
{
    
    
    ino_t  d_ino;       /* i-node节点号 */
    char   d_name[256]; /* 文件名 */
};

示例代码:

int main(void)
{
    
    
    // 打开目录,获取目录指针
    DIR *dp = opendir("a");

    // 读取每个目录项,并输出各个文件的名称
    struct dirent *ep;
    while(1)
    {
    
    
        ep = readdir(dp);

        // 读完了
        if(ep == NULL)
        {
    
    
            break;
        }

        printf("%s\n", ep->d_name);
    }
}

【19】本章问题解答

问:老师,为什么我打开一个目录之后,可以读到文件名,但是取不到文件的属性信息?我的代码是这样的:

DIR *dp = opendir("dir");
struct dirent *ep = readdir(dp);
if(ep != NULL)
{
    
    
    // 获取文件属性
    struct stat info;
    bero(&info, sizeof(info));
    stat(ep->d_name, &info); // 失败
}

答:这是因为你只是打开了目录,获取了目录指针,并没有进入目录里面,因此上述的stat函数注定是失败的。要想让程序正常运行,只需要在打开目录之后,再进入即可:

DIR *dp = opendir("dir"); // 1. 打开目录
chdir("dir");             // 2. 进入目录

另外还需要注意一点,进入了某个目录之后,就意味着改变了当前进程的路径了,如果还要想回到原来的地方,就必须在改变路径之前,先保留原先的路径:

// 保留当前的路径
char *path = calloc(1, 100);
getcwd(path, 100);

// 切换到另一个目录
chdir("dir");

// 再切换回到原先的路径
chdir(path);

【20】面试题

理解 setuid、setgid 和 stickyBit 的逻辑含义,并在 Linux 系统中创建两个用户,验证这些特殊比特位的作用。

解析:

setuid:
只对普通可执行文件有效。
对文件设置setuid,意味着该文件的所有者预先做了约定:以后不管哪个用户来执行该文件,执行期间都将获得所有者的临时授权。
setgid:
只对目录有效。
对目录设置setgid,意味着该目录的所有者预先做了约定:以后不管哪个用户来执行本目录下的可执行文件,执行期间都将获得所有者的临时授权。

操作提示:
假设有某程序createFile,对该程序执行文件设置setuid:

gec@ubuntu:~$ chmod 4777 creatFile
gec@ubuntu:~$ ls -l
-rwsrwxrwx  1 gec gec 8512 Apr 15 22:53 createFile

上述命令中,4777意味着将文件createFile的权限位设置为

s-- rwx rwx rwx
100 111 111 111

这么一来,如果有别的用户执行程序createFile去创建了文件,那么该文件的所有者将会始终是当前的gec用户,而该文件的所属组则会随着实际的用户所在的所属组变化。比如有用户jack执行了这个createFile:

gec@ubuntu:~$ su jack
jack@ubuntu:/home/gec$ ls -l
-rwsrwxrwx  1 gec gec 8512 Apr 15 22:53 createFile
jack@ubuntu:/home/gec$ ./createFile
jack@ubuntu:/home/gec$ ls -l
-rwsrwxrwx  1 gec gec 8512 Apr 15 22:53 createFile
--w---x---  1 gec jack   0 Apr 15 22:54 file

可见,jack创建的文件file却属于gec,这充分展现了setuid的作用。

「课堂练习2」
示例代码:

int main(void)
{
    
    
    // 打开目录,获取目录指针
    DIR *dp = opendir("a");

    // 读取每个目录项,并输出各个文件的名称
    struct dirent *ep;
    while(1)
    {
    
    
        ep = readdir(dp);

        // 读完了
        if(ep == NULL)
        {
    
    
            break;
        }

        printf("%s\n", ep->d_name);
    }
}

参照上述示例代码,打开目录并读取每一个目录项,达到如下要求:

不显示以 ‘.’ 开头的隐藏文件。 不仅显示文件的名称,还要显示文件的大小。
提示:打开目录并不意味着进入目录,而获取目录里面的文件的信息必须要先进入目录,可以用函数 chdir() 进入目录。

解析:

不显示以’.'开头的隐藏文件,只需要判断文件的名称的起始字符即可。 要显示文件的大小,最简单的方式是通过 lstat()/stat()
获取文件的属性信息,取里面的大小尺寸即可。

示例代码:

int main(int argc, char **argv)
{
    
    
    // 打开目录,获取目录指针
    DIR *dp = opendir(argv[1]);
    // 进入目录
    chdir(argv[1]);

    // 读取每个目录项,并输出各个文件的名称
    struct dirent *ep;
    struct stat finfo;
    while(1)
    {
    
    
        ep = readdir(dp);

        // 读完了
        if(ep == NULL)
        {
    
    
            break;
        }

        // 读到隐藏文件,跳过
        if(ep->d_name[0] == '.')
          continue;

        bzero(&finfo, sizeof(finfo));
        stat(ep->d_name, &finfo);
        printf("%s\t%d\n", ep->d_name, finfo.st_size);
    }
}

(文件属性)

【1】给定一个普通文件,使用尽可能多的方法,获取该文件的大小。

解析:

获得文件大小至少有三种办法: 按字节读取文件,统计读取次数,直到读完为止。
使用stat函数直接获取文件大小(注意,若文件包含空洞则此信息不是文件真实大小)。
使用lseek/fseek函数定位到文件末尾,然后获取文件位置即可获取文件大小。

参考代码1:

#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

int main(int argc, char **argv)
{
    
    
    FILE *fp = fopen(argv[1], "r");
    long size = 0;
    char ch;
    while(1)
    {
    
    
        int n = fread(&ch, 1, 1, fp);
        if(n == 0 && feof(fp))
        {
    
    
            printf("文件大小:%ld\n", size);
            fclose(fp);
            break;
        }
        size++;
    }
    return 0;
}

参考代码2:

#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

int main(int argc, char **argv)
{
    
    
    struct stat info;
    bzero(&info, sizeof(info));

    stat(argv[1], &info);
    printf("文件大小: %lld\n", info.st_size);

    return 0;
}

参考代码3:

#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

int main(int argc, char **argv)
{
    
    
    FILE *fp = fopen(argv[1], "r");

    fseek(fp, 0L, SEEK_END);
    printf("文件大小: %ld\n", ftell(fp));

    fclose(fp);
    return 0;
}

(目录操作、文件属性)

【2】编写一个程序,令其功能向命令 “ls -l” 靠近。
在这里插入图片描述

解析:

本题主要考察点是目录操作和文件属性的获取,注意对于符号链接文件本身的属性要用lstat来获取。另外,为了更贴近命令 ls -l
的效果,本参考代码还加入了颜色控制。

参考代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdbool.h>

#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/sysmacros.h>

#include <pwd.h>
#include <grp.h>
#include <time.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <dirent.h>

#define MAX_LINE 255

#define cyan(string) \
	do{
      
       \
		char s[MAX_LINE]; \
		readlink(string, s, MAX_LINE); \
		printf("\033[1;36m%-4s -> %s\033[m\n", string, s); \
	}while(0)

#define blue(string) \
	do{
      
      printf("\033[1;32;34m%-4s\033[m\n", string);}while(0)

#define yellowBold(string) \
	do{
      
      printf("\033[1;33;40m%-4s\033[m\n", string);}while(0)

#define yellowLight(string) \
	do{
      
      printf("\033[0;33;40m%-4s\033[m\n", string);}while(0)

#define purple(string) \
	do{
      
      printf("\033[1;35m%-4s\033[m\n", string);}while(0)

#define other(string) \
	 do{
      
      printf("%-4s\n", string);}while(0)

void err_sys(const char *info)
{
    
    
	perror(info);
	exit(1);
}

void err_quit(const char *info)
{
    
    
	fprintf(stderr, "%s", info);
	exit(2);
}

char *mode(struct stat *info)
{
    
    
	static char mode[10];

	switch(info->st_mode & S_IFMT)
	{
    
    
		case S_IFSOCK:mode[0] = 's';break;
		case S_IFLNK: mode[0] = 'l';break;
		case S_IFDIR: mode[0] = 'd';break;
		case S_IFREG: mode[0] = '-';break;
		case S_IFBLK: mode[0] = 'b';break;
		case S_IFCHR: mode[0] = 'c';break;
		case S_IFIFO: mode[0] = 'p';
	}
	
	char rwx[] = {
    
    'r', 'w', 'x'};
	int i;
	for(i=0; i<10; i++)
		snprintf(mode+i+1, 10, "%c",
			(info->st_mode&(0400>>i))?rwx[i%3]:'-');

	return mode;
}

int hln(struct stat *info)
{
    
    
	return info->st_nlink;
}

char *user(struct stat *info)
{
    
    
	struct passwd *pw;
	pw = getpwuid(info->st_uid);
	if(pw == NULL)
	{
    
    
		static char uid[8];
		snprintf(uid, 8, "%u", info->st_uid);
		return uid;
	}

	return pw->pw_name;
}

char *group(struct stat *info)
{
    
    
	struct group *gr;
	gr = getgrgid(info->st_gid);
	if(gr == NULL)
	{
    
    
		static char gid[8];
		snprintf(gid, 8, "%u", info->st_gid);
		return gid;
	}

	return gr->gr_name;
}

char *size_dev(struct stat *info)
{
    
    
	static char size_or_dev[10];

	if(((info->st_mode&S_IFMT) == S_IFBLK) ||
		((info->st_mode&S_IFMT) == S_IFCHR))
		snprintf(size_or_dev, 10,
			"%d,%4d", major(info->st_rdev), minor(info->st_rdev));
	else
		snprintf(size_or_dev, 10, "%d", (int)info->st_size);

	return size_or_dev;
}

char *mtime(struct stat *info)
{
    
    
	char *t;
	t = ctime(&info->st_mtime);
	return t;
}

void print_color_name(struct stat *info, char *path)
{
    
    
	switch(info->st_mode & S_IFMT)
	{
    
    
	case S_IFDIR:
		blue(path);break;
	case S_IFLNK:
		cyan(path);break;
	case S_IFCHR:
	case S_IFBLK:
		yellowBold(path);break;
	case S_IFIFO:
		yellowLight(path);break;
	case S_IFSOCK:
		purple(path);break;
	default:
		other(path);
	}
}

void display(struct stat *info)
{
    
    
	printf("%s%3d %7s %7s%8s %.16s ",
		mode(info),
		hln(info),
		user(info),
		group(info),
		size_dev(info),
		mtime(info));
}

int main(int argc, char **argv)
{
    
    
	if(argc >= 3)
	{
    
    
		printf("参数过多\n");
		exit(0);
	};

	char *target = ((argc==2) ? argv[1] : ".");

	struct stat info;
	bzero(&info, sizeof(info));
	if(lstat(target, &info) == -1)
	{
    
    
		perror("lstat failed");
		exit(0);
	}

	if(S_ISDIR(info.st_mode))
	{
    
    
		DIR *dp = opendir(target);
		if(chdir(target) == -1)
		{
    
    
			perror("chdir() failed");
			exit(0);
		}

		struct dirent *ep;
		while((ep=readdir(dp)) != NULL)
		{
    
    
			if(ep->d_name[0] == '.')
				continue;

			bzero(&info, sizeof(info));
			if(lstat(ep->d_name, &info) == -1)
			{
    
    
				perror("lstat failed");
				exit(0);
			}

			display(&info);
			print_color_name(&info, ep->d_name);
		}

		closedir(dp);
	}

	else
	{
    
    
		display(&info);
		print_color_name(&info, target);
	}
		
	exit(0);
}

对上述代码中出现的宏定义,用来使得printf可以输出前景色、背景色、加粗、闪烁灯各种特效,基本语法是:


\033[ 属性1;属性2;…属性N m字符串\033[m

其中:

\033[ 是定义特效属性的开始。
m\033[m 是定义特效属性的结束。

字符串是要显示出来的数据。
属性都用简单的整数来说明,具体如下:

1: 高亮或加粗
2: 淡化
3: 斜体
4: 下划线
5: 闪烁
7: 前景色与背景色调转
8: 隐藏
30: 前景色(字体颜色)为黑色
31: 前景色(字体颜色)为红色
32: 前景色(字体颜色)为绿色
33: 前景色(字体颜色)为黄色
34: 前景色(字体颜色)为蓝色
35: 前景色(字体颜色)为品红色
36: 前景色(字体颜色)为青色
37: 背景色为白色
40: 背景色为黑色
41: 背景色为红色
42: 背景色为绿色
43: 背景色为黄色
44: 背景色为蓝色
45: 背景色为品红色
46: 背景色为青色
47: 背景色为白色

示例:

// 在屏幕上输出加粗、闪烁、背景为白色且字体为红色的字符串"abcd"
printf("\033[1;5;31;47m%s\033[m\n", "abcd");

效果:

(目录操作、文件IO函数)

**

【3】编写一个程序,令其功能尽量向命令 “cp” 靠近。

**

解析:

本题需注意cp命令既可以复制普通文件,也可以复制整个目录,因此需要预先判断给定的文件类型,然后再逐个复制。
本地对目录的递归复制是一个难点,需要对进程的路径切换比较熟悉。

参考代码:

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <errno.h>

#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <dirent.h>

#define _GNU_SOURCE
#include <unistd.h>

void usage(int argc, char **argv)
{
    
    
	if(argc != 3)
	{
    
    
		printf("用法: ./copy <源文件> <目标文件>\n");
		exit(0);
	}
}

int copyfile(int fd1, int fd2)
{
    
    
	struct stat info;
	bzero(&info, sizeof(info));
	fstat(fd1, &info);

	// 大于100M的大文件分次读完
	char *buf;
	int  size, max=100*1024*1024;
	size = (info.st_size > max) ? max : info.st_size;
	buf = calloc(1, size);

	int n, m, total=0;
	while(1)
	{
    
    
		while((n=read(fd1, buf, size))==-1 &&
			errno == EINTR);

		if(n == 0)
			break;
		if(n == -1)
		{
    
    
			perror("read() 失败");
			return -1;
		}

		char *tmp = buf;
		while(n > 0)
		{
    
    
			while((m=write(fd2, tmp, n))==-1 &&
				errno == EINTR);

			n -= m;
			tmp += m;
			total += m;
		}
	}

	return total;
}

void copydir(const char *dir1, const char *dir2)
{
    
    
	DIR *dp = opendir(dir1);

	struct stat info;
	struct dirent *ep;
	while(1)
	{
    
    
		ep = readdir(dp);

		if(ep == NULL)
			break;

		if(!strcmp(ep->d_name, ".") ||
		   !strcmp(ep->d_name, ".."))
			continue;


		chdir(dir1);
		bzero(&info, sizeof(info));
		stat(ep->d_name, &info);

		if(S_ISREG(info.st_mode))
		{
    
    
			chdir(dir1);
			int fd1 = open(ep->d_name, O_RDONLY);

			chdir(dir2);
			int fd2 = open(ep->d_name,
					O_WRONLY | O_CREAT | O_TRUNC, 0644);

			copyfile(fd1, fd2);
		}

		else if(S_ISDIR(info.st_mode))
		{
    
    
			chdir(dir1);
			chdir(ep->d_name);
            char d1[100];
            bzero(d1, 100);
			getcwd(d1, 100);

			chdir(dir2);
			mkdir(ep->d_name, 0777);
			chdir(ep->d_name);
            char d2[100];
            bzero(d2, 100);
			getcwd(d2, 100);

			copydir(d1, d2);
		}

		else
		{
    
    
			printf("跳过无法拷贝的文件: %s\n", ep->d_name);
		}
	}
}

int main(int argc, char **argv)
{
    
    
	usage(argc, argv);

	struct stat info;
	bzero(&info, sizeof(info));

	stat(argv[1], &info);

	if(S_ISREG(info.st_mode))
	{
    
    
		int fd1 = open(argv[1], O_RDONLY);
		int fd2 = open(argv[2], O_WRONLY|O_CREAT|O_TRUNC, 0644);

		copyfile(fd1, fd2);
	}
	else if(S_ISDIR(info.st_mode))
	{
    
    
		char ori_path[100];
        bzero(ori_path, 100);
        getcwd(ori_path, 100);

		chdir(argv[1]);
		char src_path[100];
        bzero(src_path, 100);
        getcwd(src_path, 100);

		chdir(ori_path);
		mkdir(argv[2], 0777);
		chdir(argv[2]);
		char dst_path[100];
        bzero(dst_path, 100);
        getcwd(dst_path, 100);
		getcwd(dst_path, 100);

		copydir(src_path, dst_path);
	}
	else
	{
    
    
		printf("对不起,你指定的文件无法拷贝\n");
	}

	return 0;
}

第五章 《设备操作-显示屏》

【21】液晶屏的基本概念

像素:

屏幕上显示颜色的最小单位,英文叫pixel。注意,位图(如jpg、bmp等格式的常见图片)也是由一个个的像素点构成的,跟屏幕的像素点的概念一样。原理上讲,将一张位图显示到屏幕上,就是将图片上的像素点一个个复制到屏幕像素点上。
在这里插入图片描述

分辨率:

宽、高两个维度上的像素点数目。
分辨率越高,所需要的显存越大。
在这里插入图片描述

色深:

每个像素所对应的内存字节数,一般有8位、16位、24位或32位 GEC6818开发板的屏幕的色深是32位的
32位色深的屏幕一般被称为真彩屏,或1600万色屏。 色深决定了一个像素点所能表达的颜色的丰富程度,色深越大,色彩表现力越强。

【22】内存映射基本原理

虽然LCD设备本质上也可以看作是一个文件,在文件系统中有其对应的设备节点,可以像普通文件一样对其进行读写操作(read/write),但由于对字符设备的读写操作是以字节流的方式进行的,因此除非操作的图像尺寸刚好与屏幕尺寸完全一致,如下图所示,图片的宽高与LCD的宽高完全一致,否则将会画面会乱。
在这里插入图片描述

以下是一段直接写设备节点的“不好”的示例代码:

void bad_display()
{
    
    
    // 打开LCD设备
    int lcd = open("/dev/fb0", O_RDWR);

    // 从JPG图片中获取ARGB数据
    char *argbbuf;
    int   argbsize;
    argbsize = jpg2rgb("dogs.jpg", &argbbuf);

    // 将RGB数据直接线性灌入LCD设备节点
    write(lcd, argbbuf, argbsize);

    // ...
}

像上述代码这样,直接将数据通过设备节点 /dev/fb0
写入的话,这些数据会自动地从LCD映射内存的入口处(对应LCD屏幕的左上角)开始呈现,并且会以线性的字节流形式逐个字节往后填充,除非图像尺寸与显示器刚好完全一致,否则显示是失败的。


一般而言,图像的尺寸大小是随机的,因此更方便的做法是为LCD做内存映射,将屏幕的每一个像素点跟映射内存一一对应,而映射内存可以是二维数组,因此就可以非常方便地通过操作二维数组中的任意元素,来操作屏幕中的任意像素点了。这里的映射内存,有时被称为显存。
在这里插入图片描述

对LCD屏幕进行内存映射

如上图所示,将一块内存与LCD的像素一一对应:

LCD上面显示的图像色彩,由其对应的内存的数据决定 映射内存的大小至少得等于LCD的真实尺寸大小
映射内存的大小可以大于LCD的真实尺寸,有利于优化动态画面(视频)体验 下面是屏幕显示为红色的示例代码:

#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
#include <fcntl.h>

int main()
{
    
    
    // 打开液晶屏文件
    int lcd = open("/dev/fb0", O_RDWR);

    // 给LCD设备映射一块内存(或称显存)
    char *p = mmap(NULL, 800*480*4, PROT_WRITE,
                   MAP_SHARED, lcd, 0);

    // 通过映射内存,将LCD屏幕的每一个像素点涂成红色
    int red = 0x00FF0000;
    for(int i=0; i<800*480; i++)
        memcpy(p+i*4, &red, 4);

    // 解除映射
    munmap(p, 800*480*4);
    return 0;
}

注意,上述代码存在诸多假设,比如屏幕的尺寸是800×480、屏幕色深是4个字节、每个像素内部的颜色分量是ARGB等等,这些信息都是“生搬硬凑”的,只能适用于某一款特定的LCD屏,如果屏幕的这些参数变了,上述代码就无法正常运行了,要想让程序在其他规格尺寸的屏幕下也能正常工作,就得让程序自动获取这些硬件参数信息。


【23】屏幕参数设定

首先明确,屏幕的硬件参数,都是由硬件驱动工程师,根据硬件数据手册和内核的相关规定,填入某个固定的地方的,然后再由应用开发工程师,使用特定的函数接口,将这些特定的信息读出来。


对于GEC6818开发板而言,上述所谓“某个固定的地方”,指的是如下这些重要的结构体(节选):

struct fb_fix_screeninfo
{
    
    
    char id[16];              /* identification string eg "TT Builtin" */
    unsigned long smem_start; /* Start of frame buffer mem */
                              /* (physical address) */
    __u32 smem_len;           /* Length of frame buffer mem */
    __u32 type;               /* see FB_TYPE_*        */
    __u32 type_aux;           /* Interleave for interleaved Planes */
    __u32 visual;             /* see FB_VISUAL_*        */ 
    __u16 xpanstep;           /* zero if no hardware panning  */
    __u16 ypanstep;           /* zero if no hardware panning  */
    __u16 ywrapstep;          /* zero if no hardware ywrap    */
    __u32 line_length;        /* length of a line in bytes    */
    ...
    ...
};

struct fb_var_screeninfo
{
    
    
    __u32 xres;           /* 可见区宽度(单位:像素) */
    __u32 yres;           /* 可见区高度(单位:像素) */
    __u32 xres_virtual;   /* 虚拟区宽度(单位:像素) */
    __u32 yres_virtual;   /* 虚拟区高度(单位:像素) */
    __u32 xoffset;        /* 虚拟区到可见区x轴偏移量 */
    __u32 yoffset;        /* 虚拟区到可见区y轴偏移量 */

    __u32 bits_per_pixel; /* 色深 */

    // 像素内颜色结构
    struct fb_bitfield red;   // 红色  
    struct fb_bitfield green; // 绿色
    struct fb_bitfield blue;  // 蓝色
    struct fb_bitfield transp;// 透明度
    ...
    ...
};

struct fb_bitfield
{
    
    
    __u32 offset;   /* 颜色在像素内偏移量 */
    __u32 length;   /* 颜色占用数位长度 */
    ...
    ...
};

上述结构体的具体定义在系统的如下路径中:

/usr/include/linux/fb.h

LCD设备参数信息从底层到应用层的流动
在这里插入图片描述

如上图所示,如果板卡已经具备LCD的驱动程序,那么应用程序就可以通过 ioctl()
来检索LCD的硬件参数信息。以粤嵌GEC6818开发板配套的群创AT070TN92-7英寸液晶显示屏为例,具体代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdbool.h>
#include <errno.h>

#include <sys/types.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <linux/fb.h>
#include <fcntl.h>

int lcd;
struct fb_fix_screeninfo fixinfo; // 固定属性
struct fb_var_screeninfo varinfo; // 可变属性

void get_fixinfo()
{
    
    
    if(ioctl(lcd, FBIOGET_FSCREENINFO, &fixinfo) != 0)
    {
    
    
        perror("获取LCD设备固定属性信息失败");
        return;
    }

}

void get_varinfo()
{
    
    
    if(ioctl(lcd, FBIOGET_VSCREENINFO, &varinfo) != 0)
    {
    
    
        perror("获取LCD设备可变属性信息失败");
        return;
    }
}

void show_info()
{
    
    
    // 获取LCD设备硬件fix属性
    get_fixinfo();
    printf("\n获取LCD设备固定属性信息成功:\n");
    printf("[ID]: %s\n", fixinfo.id);
    printf("[FB类型]: ");
    switch(fixinfo.type)
    {
    
    
    case FB_TYPE_PACKED_PIXELS:      printf("组合像素\n");break;
    case FB_TYPE_PLANES:             printf("非交错图层\n");break;
    case FB_TYPE_INTERLEAVED_PLANES: printf("交错图层\n");break;
    case FB_TYPE_TEXT:               printf("文本或属性\n");break;
    case FB_TYPE_VGA_PLANES:         printf("EGA/VGA图层\n");break;
    }
    printf("[FB视觉]: ");
    switch(fixinfo.visual)
    {
    
    
    case FB_VISUAL_MONO01:             printf("灰度. 1=黑;0=白\n");break;
    case FB_VISUAL_MONO10:             printf("灰度. 0=黑;1=白\n");break;
    case FB_VISUAL_TRUECOLOR:          printf("真彩色\n");break;
    case FB_VISUAL_PSEUDOCOLOR:        printf("伪彩色\n");break;
    case FB_VISUAL_DIRECTCOLOR:        printf("直接彩色\n");break;
    case FB_VISUAL_STATIC_PSEUDOCOLOR: printf("只读伪彩色\n");break;
    }
    printf("[行宽]: %d 字节\n", fixinfo.line_length);

    // 获取LCD设备硬件var属性
    get_varinfo();
    printf("\n获取LCD设备可变属性信息成功:\n");
    printf("[可见区分辨率]: %d×%d\n", varinfo.xres, varinfo.yres);
    printf("[虚拟区分辨率]: %d×%d\n", varinfo.xres_virtual, varinfo.yres_virtual);
    printf("[从虚拟区到可见区偏移量]: (%d,%d)\n", varinfo.xoffset, varinfo.yoffset);
    printf("[色深]: %d bits\n", varinfo.bits_per_pixel);
    printf("[像素内颜色结构]:\n");
    printf("  [红] 偏移量:%d, 长度:%d bits\n", varinfo.red.offset, varinfo.red.length);
    printf("  [绿] 偏移量:%d, 长度:%d bits\n", varinfo.green.offset, varinfo.green.length);
    printf("  [蓝] 偏移量:%d, 长度:%d bits\n", varinfo.blue.offset, varinfo.blue.length);
    printf("  [透明度] 偏移量:%d, 长度:%d bits\n", varinfo.transp.offset, varinfo.transp.length);
    printf("\n");
}

int main()
{
    
    
    lcd = open("/dev/fb0", O_RDWR);
    if(lcd == -1)
    {
    
    
        perror("打开 /dev/fb0 失败");
        exit(0);
    }

    // 显示LCD设备属性信息
    show_info();

    return 0;
}

【24】 多缓冲机制

仔细观察上述显示单色的程序运行效果,会发现屏幕上的颜色不是一瞬间整体显示的,而是有一个很明显的从上到下刷屏的过程,这实际上是由于我们是一个个像素点从左到右,从上到下刷屏导致的,如果不是速度比较快,我们将会看到屏幕上的点是一个个亮起来的,而不是整屏统一更新,这显然不是最佳的体验。
在这里插入图片描述

闪屏问题

解决这个问题,可以采用多缓冲的办法,首先要搞明白所谓可见区和虚拟区的关系:


可见区、虚拟区都是内存区域,可见区是虚拟区的一部分,因此可见区尺寸至少等于虚拟区。
一般而言,可见区尺寸就是屏幕尺寸,比如800×480;而虚拟区是显示设备能支持的显存大小,比如800×480、800×960等。
为了提高画面体验,一般先在不可见区操作显存数据,然后在调整可见区位置,使得图像“瞬间”呈现,避免闪屏。

可见区与虚拟区
在这里插入图片描述

下面以示例代码的形式,来分析如何使用多缓冲机制提高画面体验。

1. 设定虚拟区

#include <stdio.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <string.h>
#include <fcntl.h>
#include <linux/fb.h>

int main()
{
    
    
    // 打开LCD设备
    int lcd = open("/dev/fb0", O_RDWR|O_EXCL);

    struct fb_var_screeninfo vinfo; // 显卡设备的可变属性结构体
    ioctl(lcd, FBIOGET_VSCREENINFO, &vinfo); // 获取可变属性

    // 获得当前显卡所支持的虚拟区显存大小
    unsigned long VWIDTH  = vinfo.xres_virtual;
    unsigned long VHEIGHT = vinfo.yres_virtual;
    unsigned long BPP = vinfo.bits_per_pixel;

    printf("虚拟区显存大小为: %d×%d\n", VWIDTH, VHEIGHT);

    // 申请一块虚拟区大小的映射内存
    char *p = mmap(NULL, VWIDTH * VHEIGHT * BPP/8,
                PROT_READ|PROT_WRITE,
                MAP_SHARED, lcd, 0); 
    if(p != MAP_FAILED)
    {
    
    
        printf("申请显存成功\n");
    }
}

在开发板运行结果:

[root@GEC6818 ~]#./a.out
虚拟区显存大小为: 800×1440
申请显存成功
[root@GEC6818 ~]#

从上述执行结果来看,粤嵌GEC6818开发板配套的群创AT070TN92-7英寸液晶显示屏支持三倍与屏幕尺寸的虚拟显存的设定。当然,在实际设定的时候,不一定要三倍,也可以是两倍大小,比如800×960。

2. 显示A区,但在B区作画

为了方便讨论,假设设定两倍屏幕尺寸的虚拟区内存,上半部分为A区,下半部分为B区。如下图所示:
在这里插入图片描述

将A区设定为可见区,代码如下:

struct fb_var_screeninfo vinfo; // 显卡设备的可变属性结构体
ioctl(lcd, FBIOGET_VSCREENINFO, &vinfo); // 获取可变属性

// 获得当前显卡所支持的虚拟区显存大小
unsigned long width  = vinfo.xres;
unsigned long height = vinfo.yres;
unsigned long bpp    = vinfo.bits_per_pixel;
unsigned long screen_size = width * height * bpp/8;

// 申请一块两倍与屏幕的映射内存
char *p = mmap(NULL, 2 * screen_size,
            PROT_READ|PROT_WRITE,
            MAP_SHARED, lcd, 0); 

// 将可见区设定为A区
vinfo.xoffset = 0;
vinfo.yoffset = 0;
ioctl(lcd, FBIOPAN_DISPLAY, &vinfo);

// 在B区绘图
int red = 0x00FF0000;
for(int i=0; i<width*height; i++)
    memcpy(p+screen_size+i*4, &red, 4);

执行上述代码,会发现虽然在B区已经填充了某些图像数据,但是屏幕上没有出现任何反应。

3. 将可见区设定为B区,瞬间出现画面,避免了“闪屏”

为了方便讨论,假设设定两倍屏幕尺寸的虚拟区内存,上半部分为A区,下半部分为B区。如下图所示:

变换可见区
在这里插入图片描述

将B区设定为可见区,代码如下:

vinfo.xoffset = 0;
vinfo.yoffset = 480;
ioctl(lcd, FBIOPAN_DISPLAY, &vinfo);

容易想到,只要交替地改变可见区,使得填充数据的过程对用户不可见,等到数据填充完毕,再通过以上代码瞬间调整可见区区域,用户就能感受到画面流程呈现的体验,避免尴尬的闪屏。


下面是完整的使用“双缓冲”机制交替呈现红绿蓝的代码及演示效果图。

#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <string.h>
#include <fcntl.h>
#include <linux/fb.h>

int main()
{
    
    
    // 打开LCD设备
    int lcd = open("/dev/fb0", O_RDWR|O_EXCL);

    struct fb_var_screeninfo vinfo; // 显卡设备的可变属性结构体
    ioctl(lcd, FBIOGET_VSCREENINFO, &vinfo); // 获取可变属性

    // 获得当前显卡所支持的虚拟区显存大小
    unsigned long width  = vinfo.xres;
    unsigned long height = vinfo.yres;
    unsigned long bpp    = vinfo.bits_per_pixel;
    unsigned long screen_size = width * height * bpp/8;

    // 申请一块两倍与屏幕的映射内存
    char *p = mmap(NULL, 2 * screen_size,
                PROT_READ|PROT_WRITE,
                MAP_SHARED, lcd, 0); 

    bzero(p, 2*screen_size);

    // 将起始可见区设定为B区
    vinfo.xoffset = 0;
    vinfo.yoffset = 480;
    ioctl(lcd, FBIOPAN_DISPLAY, &vinfo);

    int colors[] = {
    
    0x00FF0000, 0x0000FF00, 0x000000FF};
    for(int k=0,n=0;; n++,k++,k%=3)
    {
    
    
        for(int i=0; i<width*height; i++)
            memcpy(p+ screen_size*(n%2) +i*4, &colors[k], 4);

        vinfo.xoffset = 0;
        vinfo.yoffset = 480*(n%2);
        ioctl(lcd, FBIOPAN_DISPLAY, &vinfo);

        sleep(1);
    }
}

双缓冲下画面的顺滑切换

在这里插入图片描述


【25】DRM概念入门

DRM全称是 Direct Rendering Manager
,即直接渲染管理器。DRM是Linux的一个子系统,专门用来与现代图形显示卡的GPU打交道,在应用层提到DRM的时候一般指的是libdrm所提供的一套API,这套API封装了诸多对内核中DRM底层驱动的接口,极大方便了用户的使用。

下图可以比较直观的展现 DRM 在系统架构中的位置:
在这里插入图片描述

从上图可以看到,DRM是内核的子模块,它的接口一般都是通过 ioctl 来提供的,而 libdrm 就是对这些众多的 ioctl 的封装。

【26】显示屏操作

2.1 获取显示屏信息
要想正常在屏幕上显示图像,至少要获取屏幕尺寸、显存入口、色彩模式等信息。通过对 DRM 和设备操作和 libdrm 接口的封装,可以使用如下代码来获取显示屏的信息:

#include "DRMwraper.h"

// DRM设备操作结构体
struct drmHandle drm;

int main()
{
    
    
    // 打开 DRM 设备
    int lcd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC);

    // 初始化 DRM 设备,并获取显示屏的分辨率
    initDRM(lcd, &drm);
    printf("显示器尺寸: %d×%d\n", drm.width, drm.height);

    // 为显示屏添加一个FrameBuffer显存,并获取显存入口和色深等信息
    createFB(lcd, &drm);
    printf("显存入口: %p\n", drm.vaddr);
    printf("色深: %u\n", drm.pitch/drm.width);
}

以上代码中,函数 initDRM() 与 createFB() 都是为了简化对 libdrm
的接口而封装的两个函数接口,可以通过如下链接下载它们,并与应用程序一起编译。

DRMwraper.h

#include <xf86drm.h>
#include <xf86drmMode.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <errno.h>
#include <string.h>

struct drmHandle
{
    
    
    uint32_t width;  
    uint32_t height;
    uint32_t pitch;
    uint32_t handle;
    uint32_t size;
    uint8_t *vaddr;
    uint32_t fb_id;
};

extern drmModeConnector *conn;
extern drmModeRes *res;
extern drmModePlaneRes *plane_res;
extern uint32_t conn_id;
extern uint32_t crtc_id;
extern uint32_t plane_id;

void initDRM(int fd, struct drmHandle *buf);
int createFB(int fd, struct drmHandle *buf);

DRMwraper.c

#include "DRMwraper.h"
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>

drmModeConnector *conn;
drmModeRes *res;
drmModePlaneRes *plane_res;
uint32_t conn_id;
uint32_t crtc_id;
uint32_t plane_id;

void initDRM(int fd, struct drmHandle *buf)
{
    
    
    // 获取crtc、encoder、connector属性信息
    res = drmModeGetResources(fd);
    crtc_id = res->crtcs[0];
    conn_id = res->connectors[0];

    // 获取plane_id
    int aa = drmSetClientCap(fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1);

    plane_res = drmModeGetPlaneResources(fd);
    if(plane_res == NULL)
    {
    
    
        perror("drmModeGetPlaneResources() failed");
        exit(0);
    }

    plane_id = plane_res->planes[0];

    // 获取显示器尺寸
    conn = drmModeGetConnector(fd, conn_id);
    buf->width  = conn->modes[0].hdisplay;
    buf->height = conn->modes[0].vdisplay;
}


int createFB(int fd, struct drmHandle *drm)
{
    
    
    struct drm_mode_create_dumb create = {
    
    };
    struct drm_mode_map_dumb map = {
    
    };

    /* create a dumb-buffer, the pixel format is XRGB888 */
    create.width = drm->width;
    create.height = drm->height;
    create.bpp = 32;
    // 创建一块连续物理内存,大小等于显示屏的大小
    // 并获取行距、色深等信息
    drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &create);

    /* bind the dumb-buffer to an FB object */
    drm->pitch = create.pitch;
    drm->size = create.size;
    drm->handle = create.handle;
    drmModeAddFB(fd, drm->width, drm->height, 24/*depth*/, 32/*bpp*/, drm->pitch,
               drm->handle, &drm->fb_id);

    /* map the dumb-buffer to userspace */
    map.handle = create.handle;
    int k;
    if((k=drmIoctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &map)) != 0)
    {
    
    
        printf("[%d]error: %s\n", __LINE__, strerror(errno));
        exit(0);
    }

    drm->vaddr = mmap(0, create.size, PROT_READ | PROT_WRITE,
            MAP_SHARED, fd, map.offset);
    if(drm->vaddr == MAP_FAILED)
    {
    
    
        perror("mmap() failed");
        exit(0);
    }
    else
        printf("映射 DRM 设备到用户内存空间成功\n");

    return 0;
}

编译时可直接使用上述文件,如:

gec@ubuntu:~$ aarch64-linux-gcc DRMwraper.c example.c -o example

将上述代码放在RK1808和RK3399中测试,输出结果如下:

# RK3399:
gec@rk3399:~$ ./example
显示器尺寸: 1920×1080
显存入口: 0x7f91637000
色深: 4字节
gec@rk3399:~$

【27】第三方开源软件编译

在嵌入式开发中,经常需要编译、部署第三方开源库,这些开源代码一般都遵循GNU开源程序框架,使用automake系列工具产生标准的配置文件,使得不同环境下的主机都能非常方便地通过统一的“三部曲”就能编译代码,极大提高了开源场景中,各开发人员在迥异的电脑环境下的协作效率。
在这里插入图片描述

下面以 jpeg 图片编解码库 libjpeg.so 为例,介绍其从下载到使用的全过程。

【28】 JPEG库的下载

JPEG 格式有时也被称为 JPG 格式,是当今最流行的静态图片压缩格式,可以直接上 JPEG
官方网站找最新版本的编解码库,或者点击如下链接下载 9.0 版本:

jpegsrc.v9d.tar.gz

下载完了之后,放到 Linux 系统中解压,值得注意的是,最好在家目录下解压和编译、安装,不使用管理员权限,不使用 sudo
,否则很可能会导致后续的操作由于权限问题而出错。

gec@ubuntu:~$ tar xzf jpegsrc.v9d.tar.gz

解压了之后,进入对应目录可以看到jpeg编解码算法的相关源代码:

在这里插入图片描述

接着,在源码目录下进行经典的源码编译三部曲:

配置: ./configure
编译: make
安装: make install

以下逐个介绍他们。

【29】JPEG库的配置

由于每个人的电脑环境都不尽相同,比如系统版本、标准库版本等,有时还需要设定安装的路径、编译的工具链等,这些所有的工作都由源码中的
configure 来完成,执行如下命令可查看其详细使用规约:

gec@ubuntu:~/jpeg-9d$ ./configure --help

在诸多选项信息中,其中有两项是最常用的,他们是:

指定安装路径:--prefix=
指定工具链前缀:--host=
比如:
gec@ubuntu:~/jpeg-9d$ ./configure --prefix=/home/gec/tools --host=arm-linux

以上指定的含义是:

将编译后生成的库文件放入指定的目录 /home/gec/tools 中(需已存在) 指定工具链的前缀为 arm-linux
对库的安装路径,如果不加指定,那么默认的安装路径是 /usr/local
,这个地方是第三方库统一的默认安装地点,但这个地方普通用户是没有权限创建文件的,因此在 make install 的时候需要 sudo。


对工具链的前缀,如果不加指定,那么默认是不带任何前缀的系统本地工具链:gcc、cpp、ar、ld
等系列工具,但如果需要用到专门针对嵌入式板卡的交叉工具链:arm-linux-gcc、arm-linux-cpp、arm-linux-ar、arm-linux-ld
等等工具,就需要指定对应的前缀。另外要注意,这里的前缀 arm-linux
只是举例,并非所有开发板工具链前缀都如此,实际操作中要视具体情况灵活处理。


【注意】:配置成功的标志是,在源码目录中生成了 Makefile 文件:

在这里插入图片描述

【30】JPEG库的编译

有了 Makefile 文件,就可以执行如下命令,进行编译:

gec@ubuntu:~/jpeg-9d$ make

编译的过程会将所有的 *.c 文件生成对应的 *.o
文件,编译时间的长短取决于源码的规模,有些比较大型的软件在主流配置的主机中编译时间可能长达数小时。

经理:一上班就出来抽烟,人家会以为你偷懒的。
程序员:代码在编译。
经理:好的。

2小时过去了…

经理:你小子怎么还在抽烟?
程序员:代码在编译。
经理:你代码有问题吧,编了2个小时??
程序员:恩编内核,公司主机配置,理当如此。
经理:%@#$^&!*… …好吧。

【31】JPEG库的安装

编译完成后,就可以将生成的库文件,连同配套的头文件、帮助文档、配置文件、实用程序等系列文件复制到指定的目录中,这个过程被称为安装,具体如下:

gec@ubuntu:~/jpeg-9d$ make install

成功安装后,会在安装路径下看到如下文件:

gec@ubuntu:~/tools$ tree -L 2

【32】JPG编解码

2.1 编码(从 RGB 到 JPG)
当我们手上握有 RGB (比如从摄像头捕获而来的 YUV 数据),并想要将这些 RGB 数据以 JPG 图片方式存储起来(比如抓拍)的时候,就需要做 JPG 的编码操作,示例代码如下:

#include "jpeglib.h"

// 参数说明:
//   rgbdata:RGB数据指针
//   jpgFilename:编码完成后得到的 JPG 文件名
// 返回值说明:
//   成功:true
//   成功:false
bool rgb2jpg(const char *rgbdata, const char *jpgFilename)
{
    
    
	// 准备图片文件
	FILE *fp = fopen(jpgFilename, "w");
	if(fp == NULL)
	{
    
    
		printf("创建文件[%s]失败:%s\n", jpgFilename, strerror(errno));
		return false;
	}

	jpeg_stdio_dest(&cinfo, fp);

	// cinfo.image_width  = CAMERA_W;
	// cinfo.image_height = CAMERA_H;
	cinfo.input_components = 3;
	cinfo.in_color_space   = JCS_RGB;

	jpeg_set_defaults(&cinfo);
	jpeg_set_quality(&cinfo, 80, TRUE);

	jpeg_start_compress(&cinfo, TRUE);

	int row_stride = 800*3;
	JSAMPROW row_pointer[1];

	// 开始编码,并将jpg数据存入指定的图片文件中
	while(cinfo.next_scanline < cinfo.image_height)
	{
    
    
		row_pointer[0] = rgbdata + cinfo.next_scanline*800*3;
		jpeg_write_scanlines(&cinfo, row_pointer, 1);
	}

	jpeg_finish_compress(&cinfo);
	jpeg_destroy_compress(&cinfo);

	free(rgbdata);
	fclose(fp);

    return true;
}

【33】JPG解码(从 JPG 到 RGB)

当我们需要从底层着手,显示一张 JPG 图片时,就需要将 JPG 解码为 RGB,进而才可以将 RGB 送到显存让液晶屏显示。而解码 JPG
的工作,就是 libjpeg.so 库的主要功能。

具体示例代码如下:

#include "jpeglib.h"

// 参数说明:
//   jpgdata: jpg图片数据
//   jpgsize: jpg图片大小
// 返回值说明:
//   成功:指向rgb数据的指针
//   失败:NULL
char *jpg2rgb(const char *jpgdata, size jpgsize)
{
    
    
    // 1,声明解码结构体,以及错误管理结构体
    struct jpeg_decompress_struct cinfo;
    struct jpeg_error_mgr jerr;

    // 2,使用缺省的出错处理来初始化解码结构体
    cinfo.err = jpeg_std_error(&jerr);
    jpeg_create_decompress(&cinfo);

    // 3,配置该cinfo,使其从 jpgdata 中读取jpgsize个字节
    //    这些数据必须是完整的JPEG数据
    jpeg_mem_src(&cinfo, jpgdata, jpgsize);

    // 4,读取JPEG文件的头,并判断其格式是否合法
    if(!jpeg_read_header(&cinfo, true))
    {
    
    
        fprintf(stderr, "jpeg_read_header failed: "
            "%s\n", strerror(errno));
        return;
    }

    // 5,开始解码
    jpeg_start_decompress(&cinfo);

    // 6,获取图片的尺寸信息
    printf("宽:  %d\n", cinfo.output_width);
    printf("高:  %d\n", cinfo.output_height);
    printf("色深:%d\n", cinfo.output_components);

    int row_stride = minfo->width * minfo->pixel_size;

    // 7,根据图片的尺寸大小,分配一块相应的内存rgbdata
    //    用来存放从jpgdata解码出来的图像数据
    unsigned long linesize = cinfo.output_width * cinfo.out_components;
    unsigned long rgbsize  = linesize * cinfo.output_height;
    char *rgbdata = calloc(1, rgbsize);

    // 8,循环地将图片的每一行读出并解码到rgb_buffer中
    int line = 0;
    while(cinfo.output_scanline < cinfo.output_height)
    {
    
    
        unsigned char *buffer_array[1];
        buffer_array[0] = rgbdata + cinfo.output_scanline * linesize;
        jpeg_read_scanlines(&cinfo, buffer_array, 1);
    }

    // 9,解码完了,将jpeg相关的资源释放掉
    jpeg_finish_decompress(&cinfo);
    jpeg_destroy_decompress(&cinfo);

    return rgbdata;
}

第六章 《设备操作-触摸屏》

【34】 输入子系统

连接操作系统的输入设备,可不止一种,也许是一个标准PS/2键盘,也许是一个USB鼠标,或者是一块触摸屏,甚至是一个游戏机摇杆,Linux在处理这些纷繁各异的输入设备的时候,采用的办法还是找中间层来屏蔽各种细节,请看下图:

在这里插入图片描述

在Linux的内核中,对输入设备的使用,实际上运用了3大块来管理,他们分别是所谓的输入设备驱动层、输入子系统核心层,以及事件触发层。他们各自的工作分别是:


`输入设备驱动层:
每一种设备都有其特定的驱动程序,他们被妥当地装载到操作系统的设备模型框架内,封装硬件所提供的功能,向上提供规定的接口。

核心层:
此处将收集由设备驱动层发来的数据,整合之后触发某一事件。

事件触发层:
这一层是我们需要关注的,我们可以通过在用户空间读取相应设备的节点文件来获知某设备的某一个动作。`

以触摸屏为例,当手指在屏幕上滑动的时候,数据流大致是这样的:驱动层中的触摸屏驱动会源源不断地产生触摸屏相关数据,并向上递送给内核输入子系统,输入子系统进一步将这些信息规整为统一的结构体,并借助事件触发层发往对应的设备节点,至此,应用程序即可从这些设备节点读取相关信息。


值得注意的是,底层驱动产生的设备数据与上层应用读取设备数据是两个完全异步的过程,彼此之间是没有耦合和约束的,例如:当底层驱动产生的触摸屏坐标信息比应用层读取的速度要快时,应用程序将会丢失一部分坐标信息。

【35】触摸屏

在最靠近应用程序的事件触发层上,内核所获知的各类输入事件,比如键盘被按了一下,触摸屏被滑了一下等,都将被统一封装在一个叫做
input_even 的结构体当中,这个结构体定义如下:

vincent@ubuntu:/usr/include/linux/$ cat input.h -n
     1    #ifndef _INPUT_H
     2    #define _INPUT_H
     3    
     ...
     ...
    20    
    21    struct input_event {
    
    
    22        struct timeval time;
    23        __u16 type;
    24        __u16 code;
    25        __s32 value;
    26    };
    27    
    ...

该结构体有4个成员,其含义分别如下:

time:输入事件发生的时间戳,精确到微秒。时间结构体定义如下:

struct timeval
{
    
    
__time_t tv_sec; // 秒
long int tv_usec; // 微秒(1微秒 = 10-3毫秒 = 10-6秒)
};
`type:输入事件的类型。比如:
事件类型(type)	说明
EV_SYN	事件间的分割标志,有些事件可能会在时间和空间上产生延续,比如持续按住一个按键
为了更好地管理这些持续的事件,EV_SYN用以将他们分割成一个个的小的数据包。
EV_KEY	用以描述键盘,按键或者类似键盘的设备的状态变化。
EV_REL	相对位移,比如鼠标的移动,滚轮的转动等。
EV_ABS	绝对位移,比如触摸屏上的坐标值。
EV_MSC	不能匹配现有的类型,这相当于当前暂不识别的事件
比如在Linux系统中按下键盘中针对Windows系统的“一键杀毒”按键,将会产生该事件。
EV_LED	用于控制设备上的LED灯的开关,比如按下键盘的大写锁定键
会同时产生 ”EV_KEY” 和 ”EV_LED” 两个事件。
… …	… …`

code:

这个“事件的代码”用于对事件的类型作进一步的描述。比如:当发生EV_KEY事件时,则可能是键盘被按下了,那么究竟是哪个按键被按下了呢?此时查看code就知道了。当发生EV_REL事件时,也许是鼠标动了,也许是滚轮动了。这时可以用code的值来加以区分。

`事件类型(type)	事件代码(code)	说明
EV_KEY	BTN_TOUCH	触摸屏发生了按压、松开事件
EV_KEY	KEY_LEFT	键盘的左箭头发生了按压、松开事件
EV_KEY	KEY_RIGHT	键盘的右箭头发生了按压、松开事件
EV_ABS	ABS_X	触摸屏发生了x轴坐标事件
EV_ABS	ABS_Y	触摸屏发生了y轴坐标事件
EV_REL	ABS_Y	触摸屏发生了y轴坐标事件
EV_ABS	ABS_Y	触摸屏发生了y轴坐标事件`
… …	… …	… …

value:

当code都不足以区分事件的性质的时候,可以用value来确认。比如由EV_REL和REL_WHEEL确认发生了鼠标滚轮的动作,但是究竟是向上滚还是向下滚呢?再比如由由EV_KEY和KEY_F确认了发生键盘上F键的动作,但究竟是按下呢还是弹起呢?这时都可以用value值来进一步判断。

`事件类型(type)	事件代码(code)	发生值(value)	说明
EV_KEY	BTN_TOUCH	大于0	手指按压了触摸屏
EV_KEY	BTN_TOUCH	0	手指松开了触摸屏
EV_KEY	KEY_LEFT	大于0	左箭头被按下
EV_KEY	KEY_LEFT	0	左箭头被松开
EV_ABS	ABS_X	213	触摸屏产生了一个233的 x 轴坐标
EV_ABS	ABS_Y	448	触摸屏产生了一个448的 y 轴坐标
… …	… …	… …	… …`

以下代码,展示了如何从触摸屏设备节点 /dev/input/event0 中读取数据,并显示当前触摸屏的实时原始数据:

// touchPanel.c

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <errno.h>

#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <linux/input.h>

int main(int argc, char **argv)
{
    
    
    int ts = open("/dev/input/event0", O_RDONLY);

    struct input_event buf;
    while(1)
    {
    
    
        bzero(&buf, sizeof(buf));
        read(ts, &buf, sizeof(buf));

        switch(buf.type)
        {
    
    
        case EV_SYN:
            printf("------------------ SYN ---------------\n");
            break;
        case EV_ABS:
            printf("time: %u.%u\ttype: EV_ABS\t",
                buf.time.tv_sec, buf.time.tv_usec);
            switch(buf.code)
            {
    
    
            case ABS_X:
                printf("X:%u\n", buf.value);
                break;
            case ABS_Y:
                printf("Y:%u\n", buf.value);
                break;
            }
        }
    }
    return 0;
}

第七章 《库的制作与应用》

【36】 编译与ELF格式

首先,程序的编译是一个复杂的过程,虽然平时一般可以将源代码文件一步到位编译生成最终的可执行文件。但其中实际上会经过如下图所示的四个步骤:
在这里插入图片描述


预处理:解释并展开源程序当中的所有的预处理指令,此时生成 *.i 文件。
编译:词法和语法的分析,生成对应硬件平台的汇编语言文件,此时生成 *.s 文件。
汇编:将汇编语言文件翻译为对应处理器的二进制机器码,此时生成 *.o 文件。
链接:将多个 *.o 文件合并成一个不带后缀的可执行文件。

以上图的hello.c为例,逐步生成这些中间文件的编译命令是:

gec@ubuntu:~$ gcc hello.c -o hello.i -E
gec@ubuntu:~$ gcc hello.i -o hello.s -S
gec@ubuntu:~$ gcc hello.s -o hello.o -c
gec@ubuntu:~$ gcc hello.o -o hello -lc

重点关注最后一步,库文件的链接:链接实际上是将多个.o文件合并在一起的过程。这些 *.o 文件合并前是 ELF 格式,合并后也是 ELF
格式。


ELF全称是 Executable and Linkable
Format,即可执行可链接格式。ELF文件由多个不同的段(section)组成,如下图所示:
在这里插入图片描述

ELF格式的合并,实际上就是将多个文件中各自对应的段合并在一起,形成一个统一的ELF文件。在此过程中,必然需要对各个 *.o
文件中的静态数据(包括常量)、函数入口的地址做统一分配和管理,这个过程就叫做“重定位”,因此未经链接的单独的 *.o
文件又被称为可重定位文件,经过链接处理合并了相同的段的文件称为可执行文件。


库的本意是library图书馆,库文件就是一个由很多 *.o 文件堆积起来的集合。

【37】库的基本概念

库文件分为两类:静态库和动态库。如:

静态库:libx.a
动态库:liby.so

库文件的名称遵循这样的规范:

lib库名.后缀

其中,lib是任何库文件都必须有的前缀,库名就是库文件真正的名称,比如上述例子中两个库文件分别叫x和y,在链接它们的时候写成 -lx 和
-ly ,后缀根据静态库和动态库,可以是 .a 或者 .so:

静态库的后缀:.a (archive,意即档案)
动态库的后缀:.so (share object,意即共享对象)

注意不管是静态库,还是动态库,都是可重定位文件 *.o 的集合。

【38】静态库

静态库的制作:(假设要将a.c、b.c制作成静态库)
第一步,制作 *.o 原材料
gec@ubuntu:~$ gcc a.c -o a.o -c
gec@ubuntu:~$ gcc b.c -o b.o -c
第二步,将 *.o 合并成一个静态库
gec@ubuntu:~$ ar crs libx.a a.o b.o
静态库的常见操作:
查看 *.o 文件
gec@ubuntu:~$ ar t libx.a (t意即table,以列表方式列出*.o文件)
a.o
b.o

删除 *.o 文件
gec@ubuntu:~$ ar d libx.a b.o (d意即delte,删除掉指定的*.o文件)
gec@ubuntu:~$ ar t libx.a
a.o

增加 *.o 文件
gec@ubuntu:~$ ar r libx.a b.o (r意即replace,添加或替换(重名时)指定的*.o文件)
gec@ubuntu:~$ ar t libx.a
a.o
b.o

提取 *.o 文件
gec@ubuntu:~$ ar x libx.a (x意即extract,将库中所有的*.o文件释放出来)
gec@ubuntu:~$ ar x libx.a a.o (指定释放库中的a.o文件)

静态库的使用

库文件最大的价值,在于代码复用。假设在上述库文件所包含的*.o文件中,已经包含了若干函数接口,那么只要能链接这个库,就无需再重复编写这些接口,直接链接即可。

假设a.c中包含了如下函数:

// a.c
void func()
{
    
    
    printf("我是a.c中的函数func\n");
}

那么,就可以使用链接库的形式,使用这个接口:

// main.c
void func();
int main()
{
    
    
    func();
    return 0;
}

编译并运行的结果是:

gec@ubuntu:~$ gcc main.c -L/home/gec -lx -o main
gec@ubuntu:~$ ./main
我是a.c中的函数func
gec@ubuntu:~$

注意:

编译语句中的 -L/home/gec 指明库文件 libx.a 的具体位置,否则系统找不到该库文件。
编译语句中的 -lx指明要链接的库文件的具体名称,注意不包含前缀后缀。
对于静态库而言,由于编译链接时会将 main.c
所需要的库代码复制一份到最终的执行文件中,这直接导出静态库的如下特性: 执行程序在编译之后与静态库脱离关系,其执行也不依赖于静态库。
执行程序执行时由于不依赖静态库,因此也省去了运行时动态。

【39】多个库的相互依赖

假设有两个库文件:liba.a 和 libb.a,它们分别只包含了 a.o 和 b.o,假设这两个源程序有如下依赖关系:

// a.c
#incldue <stdio.h>
void fa()
{
    
    
    printf("Hey!\n");
}

// b.c
#incldue <stdio.h>
void fa();
void fb()
{
    
    
    fa(); // fb() 调用了 fa(),即libb.a依赖于liba.a
}

很明显,b.c中的功能接口是依赖于 a.c 的,换句话说,库文件 libb.a 是依赖于 liba.a 的。

现在再来写一个调用 fb() 的主函数:

void fb();
int main(void)
{
    
    
    fb();
}

编译情况如下:

gec@ubuntu:~$ gcc main.c -o main -L. -lb -la
gec@ubuntu:~$ gcc main.c -o main -L. -la -lb
./libb.a(b.o): In function `fb':
b.c:(.text+0xa): undefined reference to `fa'
collect2: error: ld returned 1 exit status
gec@ubuntu:~$ 
gec@ubuntu:~$ 

从以上编译信息来看,得出结论:

当编译链接多个库,且这些库之间有依赖关系时,被依赖的基础库要放在编译语句的后面。 在以上示例中,库 libb.a 依赖于 liba.a,即
liba.a 是被依赖的基础库,因此 -la 要放在 -lb 的后面才能通过编译。
注意:以上结论对于静态库、动态库都适用。


【40】动态库基本概念

不管是动态库还是静态库,它们都是 *.o 文件的集合。如果把一个 *.o
文件比作一本图书,那么库文件就是书店或图书馆,静态库和动态库的关系和区别是:

静态库(相当于书店,只卖不借)

原理:编译时,库中的代码将会被复制到每一个程序中
优点:程序不依赖于库、执行效率稍高
缺点:浪费存储空间、无法对用户升级迭代
动态库(相当于图书馆,只借不卖)

原理:编译时,程序仅确认库中功能模块的匹配关系,并未复制
缺点:程序依赖于库、执行效率稍低
优点:节省存储空间、方便对用户升级迭代

在这里插入图片描述

表面上看,静态库和动态库各有千秋,彼此的优缺点是相对的,但在实际应用中,动态库应用场合要远多于静态库,因为虽然动态库的运行时装载特性会使得程序性能有略微的下降,但换来的是不仅仅节省了大量的存储空间,更重要的是使得主程序和库松耦合,不互相捆绑,甚至可以随时插拔替换!这极大地提高了程序的灵活性。

【41】动态库的命名

动态库、静态库的名称都遵循这样的规范:

lib库名.后缀

对于动态库而言,在后缀后面还经常会带着版本号:

lib库名.后缀.版本号

比如系统的标准库路径下:

在这里插入图片描述

此处,符号链接的作用不是“快捷方式”,而是为了可以让动态库在升级版本的时候更加方便地向前兼容。一般而言,完整的动态库文件名称是:

lib库名.so.主版本号.次版本号.修订版本号,比如: libx.so.1.3.1

当动态库迭代升级时,其版本号会发生相应的改变。比如下面的版本更迭:

2021年3月08日发布:libx.so.1.0.0
2021年4月02日发布:libx.so.1.0.1
2021年4月23日发布:libx.so.1.0.2
2021年5月18日发布:libx.so.1.0.3
2021年8月09日发布:libx.so.1.1.0
2021年9月12日发布:libx.so.1.1.1

可以看到,修订版本号的更迭会比较频繁,次版本号次之,主版本号再次之。为了避免每次版本号的修改而重新编译,动态库一般会用一个只带主版本号的符号链接来链接程序,如:

gec@ubuntu:~$ ls -l
lrwxrwxrwx 1 root root    15 Jan 16 2020 libbsd.so.0 -> libbsd.so.0.8.7
-rw-r--r-- 1 root root 80104 Jan 16 2020 libbsd.so.0.8.7

这样一来,未来不管版本号如何变迁,只要主版本号不变,那么用户链接的库名永远都是libbsd.so.0,而无需关心具体某个版本。而如果连主版本号都发生了改变,这一般是因为库不再向前兼容,比如删除了某些原有的接口,这种情况下,用户就需要重新编译程序。

【42】 动态库的制作

不管是静态库还是动态库,都是用来被其他程序链接的一个个功能模块。与静态库一致,制作动态库的步骤如下:

将 *.c 编译生成 *.o
将 *.o 编译成动态库

例如:

gec@ubuntu:~$ ls
a.c b.c

# 第一步:将源码编译为 *.o 
gec@ubuntu:~$ gcc a.c -o a.o -c -fPIC
gec@ubuntu:~$ gcc b.c -o b.o -c -fPIC
gec@ubuntu:~$ ls
a.c b.c a.o b.o

# 第二步:将 *.o 编译为动态库
gec@ubuntu:~$ gcc -shared -fPIC -o libx.so a.o b.o
gec@ubuntu:~$ ls
a.c b.c a.o b.o libx.so

【43】 动态库的使用

动态库的编译跟静态库并无二致,如:

gec@ubuntu:~$ pwd
/home/gec
gec@ubuntu:~$ ls lib/
libx.so
gec@ubuntu:~$ gcc main.c -o main -L./lib -lx

说明:

-L 选项后面跟着动态库所在的路径。
-l 选项后面跟着动态库的名称。

运行时链接

动态库的最大特征,就是编译链接后程序并不包含动态库的代码,这些程序会在每次运行时,动态地去寻找并定位其所依赖的库文件中的模块,这是他们为什么被称为动态库的原因。

也就是说,如果程序运行时找不到动态库,运行就会失败,例如:

gec@ubuntu:~$ ./main

报错
出现上述错误的原因,就是因为运行程序 main 时,无法找到其所依赖的动态库 libx.so,解决这个问题,有三种办法:

编译时预告:

gec@ubuntu:~$ gcc main.c -o main -L. -lx -Wl,-rpath=/home/gec/lib

设置环境变量:

gec@ubuntu:~$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/gec/lib

修改系统默认库路径:(不推荐)

gec@ubuntu:~$ sudo vi /etc/ld.so.conf.d/libc.conf
gec@ubuntu:~$ sudo ldconfig

在以上文件中,添加动态库所在路径即可。 注意: 此处要小心编辑,一旦写错可能会导致系统无法启动,这是一种
污染
系统的做法,不推荐。

第八章 《动态库的参数》

【44】工程问题

提出这么一个问题:A公司为B公司的一条自动化流水线开发一个检测装置,B公司要求可以检测流水线最终的产品是否合格。具体是:


第一版需要检测产品的涂层颜色是否均匀、外观是否有破损两个指标。 系统上线运行后,支持B公司可以自主增加新的检测项目。
这个工程需求,简单地讲就是需要A公司开发的检测系统,能自动链接目前尚未出现的、未来的接口,这就需要A公司不是开发出检测外观、涂层颜色等具体功能的软件,而是要给B公司提供一个具备可拓展的软件“框架”,使得B公司后续可以按照自己的实际需求来拓展检测装置的功能。

【45】 动态加载库

动态库最大的优点,是将链接推迟到运行时,由于运行时才链接动态库,这就给链接的目标留下了选择的空间。结合以上工程需求,可以让程序在运行的时候,为其指定要链接的动态库,以达到可以按需链接动态库的目的,这种做法称为动态库的动态加载。

具体做法如下:

约定好函数接口,比如 void detection()
将各个不同需求的实现代码封装到不同的库中,比如libcolor.so、libshape.so
编写相应配置文件,指定程序在启动后要链接的具体的库

【46】具体实现

接口实现:

// a.c
void detection()
{
    
    
    printf("正在检测颜色是否均匀...\n");
}

// b.c
void detection()
{
    
    
    printf("正在检测外观是否破损...\n");
}

将不同的功能模块制作成动态库:

gec@ubuntu:~$ gcc a.c -o a.o -c -fPIC
gec@ubuntu:~$ gcc -shared -fPIC -o libcolor.so a.o
gec@ubuntu:~$ 
gec@ubuntu:~$ gcc b.c -o b.o -c -fPIC
gec@ubuntu:~$ gcc -shared -fPIC -o libshape.so b.o
gec@ubuntu:~$ 
gec@ubuntu:~$ ls
libcolor.so  libshape.so

编写一个配置文件,指定程序需要加载的动态库:

gec@ubuntu:~$ cat config
libcolor.so

读取配置文件,并根据指示加载指定的动态库:

#include <stdio.h>
#include <dlfcn.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdbool.h>

#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>

int main(int argc, char **argv)
{
    
    
	// 读取配置文件
	FILE *fp = fopen("config", "r");
	char *lib = calloc(1, 30);
	fgets(lib, 30, fp);
	fclose(fp);

	// 根据配置文件打开指定的库
	void *handle = dlopen(strtok(lib, "\n"), RTLD_NOW);
	if(handle == NULL)
	{
    
    
		printf("加载动态库[%s]失败:%s\n", lib, strerror(errno));
		exit(0);
	}

	// 在库中查找事先约定好的接口
	void (*detect)(void);
	detect = dlsym(handle, "detect");
	if(detect == NULL)
	{
    
    
		printf("查找符号[%s]失败:%s\n", "detect", strerror(errno));
		exit(0);
	}

	// 潇洒地调用该接口
	detect();
}

【47】代码解析

打开和关闭动态库,获取动态库的操作句柄:
在这里插入图片描述

关键点:

RTLD_LAZY意味着打开动态库时,并不立即解析库中的函数符号的内存位置,而是等待程序实际调用时才临时去解析。
RTLD_NOW与上述含义相反,它意味着打开动态库时就立即解析库中的函数符号的内存位置。
不管是LAZY还是NOW,库中的静态数据符号都将被立即解析。
在这里插入图片描述

关键点:

该函数用于在动态库中获取指定的函数入口地址。

第九章 《项目阶段》

小区信息展示屏

  1. 项目描述
    1.1 背景 在很多小区中,物业为了方便管理,同时给业主提供一定的生活便利,通常会将一些通知、天气、欢迎语等在屏幕中展示,如下图所示。 要求使用已学知识,利用开发板的液晶屏,做一个类似小区信息播报屏的产品。

1.2 必做要求 可以展示指定的文本信息到屏幕上 支持中文

1.3 选做要求 展示系统时间 展示实时天气 滚动播报

猜你喜欢

转载自blog.csdn.net/m0_45463480/article/details/124739345