赞
踩
《【正点原子】I.MX6U嵌入式Linux C应用编程指南》学习笔记
静态文件与 inode
文件在没有打开时,都是存放在磁盘等存储设备中并且以一种固定的形式存放,我们把它们称为静态文件。我们在程序中调用 open() 函数是如何找到文件所在的存储位置呢,这里就要提到 inode 的概念了。
inode 是 UNIX 操作系统中的一种数据结构,其本质是结构体,它包含了与文件系统中各个文件相关的一些重要信息。在 UNIX 中创建文件系统时,同时将会创建大量的 inode 。通常,文件系统磁盘空间中大约百分之一空间分配给了 inode 表。
下面的定义仅给出了 inode 中所包含的、UNIX 用户经常使用的一些重要信息:
● inode 编号
● 用来识别文件类型,以及用于 stat C 函数的模式信息
● 文件的链接数目
● 属主的ID (UID)
● 属主的组 ID (GID)
● 文件的大小
● 文件所使用的磁盘块的实际数目
● 最近一次修改的时间
● 最近一次访问的时间
● 最近一次更改的时间——百度百科
打开一个文件,系统内部将会进行以下三步:
平时我们编写代码时,判断函数执行失败后,会使用 return 退出程序,但是很难确定具体出错的原因(一般出错时我们都返回 -1)。Linux 系统下对常见错误编了号,分别对应不同的错误类型,这些错误编号赋值给了 errno 变量。
errno 是记录系统的最后一次错误代码。代码是一个int型的值,在errno.h中定义。查看错误代码errno是调试程序的一个重要方法。当linux C api函数发生异常时,一般会将errno变量(需include errno.h)赋一个整数值,不同的值表示不同的含义,可以通过查看该值推测出错的原因。在实际编程中用这一招解决了不少原本看来莫名其妙的问题。
——百度百科
下面举个简单的例子,当前目录没有 “test_file” 这个文件,所以会运行 if 内的语句。
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <errno.h> int main() { int fd; fd = open("test_file", O_RDWR); if(fd < 0) { //printf("test_file open failed.\n"); printf("%d\n", errno); return -1; } }
这个程序的运行结果为 2(前提是 test_file 文件不存在)。错误码 2 对应的错误类型为 “文件或目录不存在”,错误码对应的含义可以在 /usr/include/asm-generic/errno.h
及errno-base.h
中查看(ubuntu16.04)。
strerror()
前面提到的 errno 只是一个数字,如果每次都要查看错误号对应的含义,未免太过麻烦。这里介绍一个 C 库函数 strerror()
,该函数可以直接获取错误码对应的错误信息(字符串形式),该函数原型如下:
#include <string.h>
char *strerror(int errnum);
下面是一个简单的测试代码:
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <errno.h> #include <string.h> int main() { int fd; fd = open("test_file", O_RDWR); if(fd < 0) { //printf("test_file open failed.\n"); printf("%s\n", strerror(errno)); return -1; } }
运行结果:
perror()
除了 strerror() 函数,perror() 函数也能用来查看错误信息,而且它比 strerror() 更加方便,strerror() 还需要传入 errno 且只能获取字符串,而 perror() 可以直接获取错误信息且将其打印出来。函数原型如下:
#include <stdio.h>
void perror(const char *s);
参数 s
是错误的自定义提示信息。
简单的测试代码:
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> int main() { int fd; fd = open("test_file", O_RDWR); if(fd < 0) { //printf("test_file open failed.\n"); perror(""); perror("错误提示"); return -1; } }
运行结果:
如果 perror() 的参数不为空字符串,那么该函数会自动在提示信息后面加上冒号和空格。
在程序遇到错误时,我们有时会使用 return 将程序终止,一般情况下,正常退出使用 return 0,错误返回 return -1。在 Linux 中,进程正常退出除了可以使用 return 外,还能用 exit()、_exit() 及 _Exit()。
_exit() 和 _Exit()
这两个函数是等价的,原型分别是:
#include <unistd.h>
void _exit(int status);
#include <stdlib.h>
void _Exit(int status);
这两个函数会结束当前进程,并且清除其使用的内存空间,关闭进程的所有文件描述符。
这里拿 _exit() 来做一个简单测试:
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <unistd.h> int main() { int fd; fd = open("test_file", O_RDWR); if(fd < 0) { //printf("test_file open failed.\n"); //perror(""); perror("错误提示"); _exit(-1); } close(fd); _exit(0); }
运行结果,$? 为上一次函数调用的返回值。
exit()
exit() 是一个标准 C 库函数,而 _exit() 和 _Exit() 是系统调用,执行 exit() 时,最后也会执行 _exit((),只不过 exit() 还会多做一些清理工作。exit() 原型如下:
#include <stdlib.h>
void exit(int status);
该函数用法和 _exit() 相同,但原文推荐我们用 exit()。
在UNIX文件操作中,文件位移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将延长该文件,并在文件中构成一个空洞,这一点是允许的。位于文件中但没有写过的字节都被设为 0。
如果 offset 比文件的当前长度更大,下一个写操作就会把文件“撑大(extend)”。这就是所谓的在文件里创造“空洞(hole)”。没有被实际写入文件的所有字节由重复的 0 表示。空洞是否占用硬盘空间是由文件系统(file system)决定的。
主要特点
- 用ls查看的文件大小是将空洞算在内的。
- cp命令拷贝的文件,空洞部分不拷贝,所以生成的同样文件占用磁盘空间小
- 用read读取空洞部分读出的数据是0,所以如果用read和write拷贝一个有空洞的文件,那么最终得到的文件没有了空洞,空洞部分都被0给填充了,文件占用的磁盘空间就大了。不过文件大小不变。
空洞文件作用很大,例如迅雷下载文件,在未下载完成时就已经占据了全部文件大小的空间,这时候就是空洞文件。下载时如果没有空洞文件,多线程下载时文件就都只能从一个地方写入,这就不是多线程了。如果有了空洞文件,可以从不同的地址写入,就完成了多线程的优势任务——百度百科
O_APPEND 和 O_TRUNC 是 oepn() 函数的两个标志。
O_TRUNC 标志
使用 O_TRUNC 这个标志,文件打开时就会把文件原来的数据丢弃。下面是一个简单的测试代码,(文件 test_file 原来是有数据的)
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <unistd.h> int main() { int fd; fd = open("test_file", O_RDWR|O_TRUNC); if(fd < 0) { perror("错误提示"); _exit(-1); } close(fd); _exit(0); }
运行结果如下,原本 test_file 内有 2322 字节的数据,使用 O_TRUNC 标志打开文件后,文件数据被清空:
O_APPEND 标志
如果 oepn 函数使用了 O_APPEND 标志,当使用 write() 函数对文件进行写操作时,文件指针会先被移动到文件末尾。下面是测试代码:
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <unistd.h> char buff[] = "ccc"; int main() { int fd; fd = open("test_file", O_RDWR|O_APPEND); if(fd < 0) { perror("错误提示"); _exit(-1); } /* 写入数据 */ write(fd, buff, sizeof(buff)); close(fd); _exit(0); }
测试结果如下,运行程序,“ccc” 被追加写入到了 test_file 中,但不知为什么总是要隔一个回车(难道文件尾带一个换行?也可能是因为 echo)
小提示:
在 Linux 系统中,可以使用 dup() 或 dup2() 这两个系统调用对文件描述符进行复制,复制得到的文件描述符和旧的文件描述符拥有相同的属性。
dup()
dup() 用于复制文件描述符,函数原型如下:
#include <unistd.h>
int dup(int oldfd);
参数 oldfd 为要复制的文件描述符。
函数运行成功时返回一个新的文件描述符,如果复制失败则返回 -1。
下面是一个简单的测试:
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <unistd.h> #include <string.h> char buff1[] = "Hi, I am fd1\n"; char buff2[] = "Hi, I am fd2\n"; int main() { int fd1, fd2; int cnt = 0; /* 打开文件 */ fd1 = open("test_file", O_RDWR|O_TRUNC); if(fd1 < 0) { perror("错误提示"); _exit(-1); } else printf("旧的文件描述符为%d\n", fd1); /* 复制文件描述符 */ fd2 = dup(fd1); if(fd2 < 0) { perror("错误提示"); _exit(-1); } else printf("新的文件描述符为%d\n", fd2); /* fd1 写入数据 */ cnt = write(fd1, buff1, sizeof(buff1)); printf("成功写入%d字节\n", cnt); /* fd2 写入数据 */ cnt = write(fd2, buff2, sizeof(buff2)); printf("成功写入%d字节\n", cnt); close(fd1); close(fd2); _exit(0); }
测试结果如下,两个文件描述符都能向 test_file 写入数据。
dup2()
dup() 和 dup2() 功能相同,惟一的区别是 dup() 返回的文件描述符由系统分配,而 dup2() 返回的文件描述符可以手动指定。下面是 dup2() 的原型:
#include <unistd.h>
int dup2(int oldfd, int newfd);
oldfd 为需要复制的文件描述符,newfd 为指定函数要返回的文件描述符。函数运行成功返回一个新的文件描述符,如果失败则返回 -1。
测试代码如下:
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <unistd.h> #include <string.h> char buff1[] = "Hi, I am fd1\n"; char buff2[] = "Hi, I am fd2\n"; int main() { int fd1, fd2; int cnt = 0; /* 打开文件 */ fd1 = open("test_file", O_RDWR|O_TRUNC); if(fd1 < 0) { perror("错误提示"); _exit(-1); } else printf("旧的文件描述符为%d\n", fd1); /* 复制文件描述符 */ fd2 = dup2(fd1, 66); if(fd2 < 0) { perror("错误提示"); _exit(-1); } else printf("新的文件描述符为%d\n", fd2); /* fd1 写入数据 */ cnt = write(fd1, buff1, sizeof(buff1)); printf("成功写入%d字节\n", cnt); /* fd2 写入数据 */ cnt = write(fd2, buff2, sizeof(buff2)); printf("成功写入%d字节\n", cnt); close(fd1); close(fd2); _exit(0); }
运行结果如下图所示,新的文件描述符为 dup2() 指定的 66。
文件共享多用于多进程多线程编程环境中,它能减少文件读写时间、提升效率。
常见的三种文件共享的实现方式:
竞争冒险简介
本小节给大家竞争冒险这个概念,如果学习过 Linux 驱动开发的读者对这些概念应该并不陌生,也就意味着竞争冒险不但存在于 Linux 应用层、也存在于 Linux 内核驱动层。
假设有两个独立的进程 A 和进程 B 都对同一个文件进行追加写操作(也就是在文件末尾写入数据),每一个进程都调用了 open 函数打开了该文件,但未使用 O_APPEND 标志,此时,每个进程都有它自己的进程控制块 PCB,有自己的文件表(意味着有自己独立的读写位置偏移量),但是共享同一个 inode 节点(也就是对应同一个文件)。假定此时进程 A 处于运行状态,B 未处于等待运行状态,进程 A 调用了 lseek 函数,它将进程 A 的该文件当前位置偏移量设置为 1500 字节处(假设这里是文件末尾),刚好此时进程 A 的时间片耗尽,然后内核切换到了进程 B,进程 B 执行 lseek 函数,也将其对该文件的当前位置偏移量设置为 1500 个字节处(文件末尾)。然后进程 B 调用 write 函数,写入了 100 个字节数据,那么此时在进程 B 中,该文件的当前位置偏移量已经移动到了 1600 字节处。B 进程时间片耗尽,内核又切换到了进程 A,使进程 A 恢复运行,当进程 A 调用 write 函数时,是从进程 A 的该文件当前位置偏移量(1500 字节处)开始写入,此时文件 1500 字节处已经不再是文件末尾了,如果还从 1500字节处写入就会覆盖进程 B 刚才写入到该文件中的数据。
以上给大家所描述的这样一种情形就属于竞争状态(也成为竞争冒险),操作共享资源的两个进程(或线程),其操作之后的所得到的结果往往是不可预期的,因为每个进程(或线程)去操作文件的顺序是不可预期的,即这些进程获得 CPU 使用权的先后顺序是不可预期的,完全由操作系统调配,这就是所谓的竞争状态。——原文
原子操作
原子操作指的是不会被其他任务打断的一种操作。上面的例子中,在 lseek() 和 write() 执行的过程中可能被打断,所以这两个操作合起来不能称为原子操作。下面介绍一些原子操作举例:
#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
fcntl() 函数
fcntl() 可以对一个已经打开的文件描述符进行一系列控制操作,如复制文件描述符、获取/设置文件描述符标志、获取/设置文件状态标志等。该函数的原型如下:
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ )
该函数的参数是可变长度,搭配不同的 cmd 有不同的参数选项,具体用法可以使用 man 手册查看:
ioctl()
ioctl() 是一个文件 IO 操作的杂物箱,一般用于操作特殊文件或硬件外设,原型如下:
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
request 参数没有统一值,可变参数 … 由 request 参数决定。
该函数后面再研究。
系统调用 truncate() 和 ftruncate() 可以将普通文件截断为指定字节长度,它们的函数原型如下:
#include <unistd.h>
#include <sys/types.h>
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);
这两个函数只有第一个参数不同,一个是以文件路径为参数,另一个是根据文件描述符匹配文件。调用成功返回 0,失败返回 -1。
下面是一个简单的测试程序:
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <unistd.h> #include <string.h> char buff[] = "hello world, I am test_file"; char tmp[50]; int main() { int fd; int cnt = 0; fd = open("test_file", O_RDWR|O_TRUNC); if(fd < 0) { perror("错误提示"); _exit(-1); } /* 写入数据 */ cnt = write(fd, buff, sizeof(buff)); printf("成功写入%d字节\n", cnt); /* 读取文件内容 */ memset(tmp, 0, sizeof(tmp)); lseek(fd, 0, SEEK_SET); cnt = read(fd, tmp, sizeof(tmp)); printf("成功读取%d字节\n", cnt); printf("截断前文件内容为:%s\n", tmp); /* 使用 truncate 将 test_file 截断为 11 字节 */ truncate("test_file", 11); /* 读取文件内容 */ memset(tmp, 0, sizeof(tmp)); lseek(fd, 0, SEEK_SET); cnt = read(fd, tmp, sizeof(tmp)); printf("成功读取%d字节\n", cnt); printf("第一次截断后文件内容为:%s\n", tmp); /* 使用 ftruncate 将 test_file 截断为 5 字节 */ ftruncate(fd, 5); /* 读取文件内容 */ memset(tmp, 0, sizeof(tmp)); lseek(fd, 0, SEEK_SET); cnt = read(fd, tmp, sizeof(tmp)); printf("成功读取%d字节\n", cnt); printf("第二次截断后文件内容为:%s\n", tmp); close(fd); _exit(0); }
测试结果,
每次每次截断后,文件指针不会改变偏移位置,所以需要我们手动调整。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。