文章目录
前言
在Linux系统中,一切皆文件。因此,掌握Linux下文件IO常用的函数、理解读写文件背后的原理至关重要。在【嵌入式Linux笔记】第三篇:Linux应用开发基础(上)中,本人已经记载了相关的文件IO的知识,但那篇只是用于快速入门,记录的知识并不全面。本篇更加细致地记录文件IO中的重要知识点,若涉及版权问题,请联系本人删除。
一、文件IO常用函数
文件IO操作流程:首先调用open函数打开文件,其次采用read/write函数进行读写操作,最后调用close函数关闭文件。
值得注意的是:调用open函数之后,内核就会在进程中建立一个打开文件的数据结构,记录打开的文件;然后内核去磁盘(块设备)中找到文件,将其加载到内存中,我们读写都是作用于内存中的文件;最后调用close函数才会将内存中的文件写入到块设备中。
这样设计的原因:块设备本身有读写限制,读写操作起来不灵活;内存以字节为单位,操作灵活方便。
1. open函数
【1】头文件:#include <sys/types.h>、#include <sys/stat.h>、#include <fcntl.h>
【2】函数原型:
- int open(const char *pathname, int flags);
- int open(const char *pathname, int flags, mode_t mode);
【3】功能:打开或创建一个文件
【4】相关描述:
①pathname:指明了文件打开的地址。
②flags:必须包含以下之一:O_RDONLY, O_WRONLY, O_RDWR。当有多个参数时,用|来分隔。其余如下的flags是可选的:
- O_APPEND:追加内容
- O_TRUNC:截断,如果文件存在并且以只写、读写方式打开,则将其长度截断为0
- O_CREAT:若文件不存在,则创建
- O_EXCL:若要创建的文件已存在,则出错并返回-1,同时修改errno的值
- O_NONBLOCK:以非阻塞方式打开设备文件
- O_SYNC:write函数阻塞等待内容全部写入块设备中才返回
- 注意:如果 O_APPEND | O_TRUNC 则为 O_TRUNC 的效果
③mode:只有当创建新文件时(使用了O_CREAT时)才使用,用于指定文件的访问权限,能用权限的数字表示法。
- umask能够设置文件在创建时mode的掩码。例如,使用umask命令得到"0002"的结果,那么文件创建时模式掩码为"000 000 000 010"。其实就是other用户权限总数字-2。
- 使用open创建文件时最终的权限结果为:"mode & ~umask"。因此,若指定权限为0777但创建文件的权限结果为"000 111 111 101"即0775.
④返回值:
- 打开成功:是一个文件描述符,其是一个小的、非负整数。成功调用返回的文件描述符是当前未为进程打开的编号最低的文件描述符。
- 打开失败:-1
⑤机制:调用open函数会创建一个新的打开文件描述,打开文件描述记录了文件偏移量和文件状态标志。文件描述符就是对该打开文件描述的参考。当路径名被删除或修改为其它文件,则此参考并不受影响。
【5】open函数打开文件:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
int main(int argc, char **argv)
{
//命令检测
if (argc != 2) {
printf("请输入命令:%s <filename>\n", argv[0]);
return -1;
}
//打开文件
int fd = open(argv[1], O_RDWR);
if (fd < 0) {
perror("错误信息");
return -1;
}
//执行相关操作
printf("打开文件成功,fd=%d\n", fd);
//关闭文件
close(fd);
return 0;
}
【6】open函数创建文件:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
int main(int argc, char **argv)
{
//命令检测
if (argc != 2) {
printf("请输入命令:%s <filename>\n", argv[0]);
return -1;
}
//打开文件,若不存在则创建权限为777
//但是系统保护,最终创建权限为775
int fd = open(argv[1], O_RDWR | O_CREAT, 0777);
if (fd < 0) {
perror("错误信息");
return -1;
}
//执行相关操作
printf("打开文件成功,fd=%d\n", fd);
//关闭文件
close(fd);
return 0;
}
2. close函数
【1】头文件:#include <unistd.h>
【2】函数原型:int close(int fd);
【3】功能:关闭文件描述符为fd的文件。
【4】返回值:关闭成功为0,关闭失败为-1
3. write函数
【1】头文件:#include <unistd.h>
【2】函数原型:ssize_t write(int fd, const void *buf, size_t count);
【3】功能:将buf中指定字节数count的内容写入文件描述符为fd的文件中。
【4】相关描述:
- ①返回值:
- 写入成功:返回写入的字节数
- 写入失败:返回小于写入字节数
- 发生错误:返回-1
- ②使用lseek函数给指定位置写入内容,是对原有位置内容进行覆盖。
【5】示例:命令行中用户输入运行命令时,给出多条字符串,将字符串写入指定文件中。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
int main(int argc, char **argv)
{
/* 1.命令格式判定 */
if (argc <= 2) {
printf("请输入命令:%s <文件名> 字符串1 ...\n", argv[0]);
return -1;
}
/* 2.打开文件 */
int fd = open(argv[1], O_RDWR | O_CREAT, 0664);
if (fd < 0) {
perror("打开文件失败");
return -1;
}
/* 3.写入字符串 */
for (int i = 2; i < argc; ++i) {
int writeLen = write(fd, argv[i], strlen(argv[i]));
if (writeLen < strlen(argv[i])) {
perror("写入失败");
break;
}
write(fd, "\n", 1);//每个字符串后再换行
}
/* 4.关闭文件 */
close(fd);
return 0;
}
4. read函数
【1】头文件:#include <unistd.h>
【2】函数原型:ssize_t read(int fd, void *buf, size_t count);
【3】功能:将文件描述符为fd的文件中指定字节数count的内容读取到buf中。
【4】返回值:读取成功返回字节数,发生错误返回-1。在判定读取是否成功时,最好不要拿返回的字节数与指定的读取字节数作比较,因为有时指定的字节数会比返回的字节数大。
【5】示例:
#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char **argv)
{
/* 1.命令格式判定 */
if (argc != 2) {
printf("请输入如下格式:%s <文件名>\n", argv[0]);
return -1;
}
/* 2.打开文件:只读 */
int fd = open(argv[1], O_RDONLY);
if (fd < 0) {
perror("打开失败");
return -1;
}
/* 3.读取文件中所有内容 */
char readBuf[100];
int readLen = read(fd, readBuf, sizeof(readBuf));
if (readLen == -1) {
perror("读取失败");
close(fd);
return -1;
}
printf("读取的文本长度:%d\n", readLen);
printf("读取的文本内容:%s", readBuf);
/* 4.关闭文件 */
close(fd);
return 0;
}
5. dup函数
【1】头文件:#include <unistd.h>
【2】函数原型:int dup(int oldfd);
【3】功能:复制。将生成的文件描述符指向传入的oldfd指向的结构体,而不会新建一个结构体。
【4】返回值:新生成的文件描述符。
【5】示例:假设本地已有一个1.txt文件,当我们通过下列程序去读1.txt文件时,执行结果为:①fd = 3, fd2 = 4, fd3 = 5 ②fd: 1, fd2: 1, fd3 : 2
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char **argv)
{
//命令行判定
if (argc != 2) {
printf("请输入下列格式:%s <文件名>\n", argv[0]);
return -1;
}
//dup功能检验
int fd = open(argv[1], O_RDONLY); //会新建一个结构体
int fd2 = open(argv[1], O_RDONLY); //会新建一个结构体
int fd3 = dup(fd); //不新建,而是指向fd的结构体
printf("fd = %d, fd2 = %d, fd3 = %d\n", fd, fd2, fd3);
//读取同一个文件
char c1, c2, c3;
read(fd, &c1, 1); //fd中的pos往后移动
read(fd2, &c2, 1); //fd2中的pos往后移动
read(fd3, &c3, 1); //由于fd3指向fd结构体,fd中的pos往后移动
printf("fd: %c, fd2: %c, fd3 : %c\n", c1, c2, c3);
//关闭文件
close(fd);
close(fd2);
return 0;
}
6. dup2函数
【1】头文件:#include <unistd.h>
【2】函数原型:int dup2(int oldfd, int newfd);
【3】功能:重定向。以dup2(fd, 1)为例说明:①关闭文件句柄1的文件;②将文件句柄1指向fd对应的file结构体。因此,之后涉及文件句柄1的操作都作用于fd所指的文件。如下图所示:
【4】返回值:newfd。 一般不关注这个返回值。
【5】示例:假设我们有个1.txt文件,执行下列程序后,命令行中并未出现字符串,而字符串"fd = 3, fd = 1"出现在了1.txt文件中。
int fd = open(argv[1], O_RDWR);
int fd2 = dup2(fd, 1);
printf("fd = %d, fd2 = %d\n", fd, fd2);
close(fd);
7. lseek函数
【1】头文件:#include <sys/types.h>、#include <unistd.h>
【2】函数原型:off_t lseek(int fd, off_t offset, int whence);
【3】参数说明:
- fd:操作的文件描述符
- offset:相对位置的偏移量,可正可负
- whence:表示从何处开始偏移,包含SEEK_SET (文件开始)、SEEK_CUR (当前位置)、SEEK_END (文件结尾)
【4】功能:将fd对应的文件中的文件指针pos位置偏移到对应位置。
【5】常见操作:
- 查看文件指针当前位置:lseek(fd, 0, SEEK_CUR);
- 查看文件共多少个字节:lseek(fd, 0, SEEK_END); 计算文件的长度、大小(以Byte为单位)
- 跳转到文件开始位置:lseek(fd, 0, SEEK_SET);
- 拓展文件长度:lseek(fd,1000,SEEK_END); 但是需要执行一次write操作才有效。
【6】空洞文件:
- 当我们用lseek偏移了几个位置后再write,那么跳过的这几个位置都是空(ASCII码为0),该文件就是空洞文件。
- 空洞文件可以用于多线程写文件,能够加快写入速度。可以将文件分成多个块,然后使用多个线程并行写入这些块。
8. fcntl函数
【1】头文件:#include <unistd.h>、#include <fcntl.h>
【2】函数原型:int fcntl(int fd, int cmd, ... /* arg */ );
【3】参数说明:
- fd:操作的文件描述符
- cmd:表示进行哪种操作。常用F_DUPED复制文件描述符,返回一个>=arg的文件描述符。
- arg:配合cmd
【4】功能:多功能文件管理工具箱。
【5】示例:fd2和fd操作同一个文件。
int fd = open(argv[1], O_RDWR | O_CREAT, 0664);
int fd2 = fcntl(fd, F_DUPFD, 0);
write(fd2, "hello", strlen("hello"));
二、文件读写细节
1. 换行符
- Windows中:换行为"\r\n"
- Linux中:换行为"\n"
- Mac中:换行为"\r"
- Linux中以16进制形式查看文件的命令:hexdump -C 文件名
2. 文件描述符
- 文件描述符是一种文件的"ID"。通过文件描述符,我们可以对其指向的文件进行读写操作。
- 在一个进程中,每次调用open函数打开一个文件,都会生成一个新的文件描述符(不会与已有的重复)。
- 不同进程的文件描述符是相互独立的。
- 一个进程中默认会有三个文件描述符:0指向标准输入(键盘),1指向标准输出(命令行),2指向标准错误的输出(命令行)。
- 文件描述符表存储的最大数量默认为1024,但是用户可以修改(视硬件配置而定)。
3. errno和perror
- errno是一个int型的全局变量,保存最近一次的错误码。
- perror是一个错误打印函数,其内部会读取errno的值然后打印对应的错误信息。例如:perror("错误"); -----> 当出现错误时,就会打印"错误:XXXX"。
4. 系统IO和用户IO
- 系统IO函数:open、close、read、write、lseek等
- 用户IO函数:fopen、fclose、fread、fwrite等
- 用户IO函数是为应用层设计的,其内部机制依旧调用了系统IO函数,但是并不是每次读写都会调用系统IO函数去访问内核。而是维护一个缓冲区,读写都是针对该缓冲区,只有当某些条件达到时才会访问内核将缓冲区的数据写入。这样设计的目的:减少用户态和内核态的切换,提高效率。
5. Linux管理文件
- 硬盘文件:一块硬盘分为两个区域:硬盘内容管理表、存储内容区域。管理表中每个文件对应唯一的inode,每个inode对应一个结构体,里面记录了文件的各种信息。
- 内存文件:在一个进程中会存在一个进程信息表记录了这个进程的所有信息,其中有一个指针指向文件管理表。文件管理表记录了所有在该进程中打开的文件信息,通过文件的fd就能找到指定文件的vnode。vnode记录了打开的文件的各种信息。
6. 文件共享
- 文件共享是指多个fd同时调用open函数打开同一个文件。
- 三种情况:
- 同一个进程中多次同时调用open打开同一个文件。
- 不同进程中打开同一个文件。
- 调用dup生成fd指向同一个文件。
三、文件属性
1. Linux文件类型
符号表示 | 含义 | 举例 |
- | 普通文件 | .txt、.c、.h |
d | 目录 | |
c | 字符设备文件(面向流) | 鼠标、键盘、串口 |
b | 块设备 | U盘、SD卡 |
p | 管道文件 | |
s | 套接字文件 | |
l | 软链接文件 |
2. 获取文件属性
2.1 stat、fstat、lstat函数:
- Linux下输入命令"stat 文件名"可以查看文件的属性。
- fstat与stat区别:stat是通过文件名得到文件属性信息的结构体,而fstat是通过fd得到属性信息。
- lstat与上述两个区别:对于软连接文件,上述两个是查看软连接指向文件的属性;而lstat是查看软连接文件本身的属性。
- 函数原型:
- int stat(const char *pathname, struct stat *statbuf);
- int fstat(int fd, struct stat *statbuf);
- int lstat(const char *pathname, struct stat *statbuf);
- 返回值:都是成功了返回0,失败返回-1
2.2 struct stat介绍:
//来自爱编程的大丙
struct stat {
dev_t st_dev; // 文件的设备编号
ino_t st_ino; // inode节点
mode_t st_mode; // 文件的类型和存取的权限, 16位整形数 -> 常用
nlink_t st_nlink; // 连到该文件的硬连接数目,刚建立的文件值为1
uid_t st_uid; // 用户ID
gid_t st_gid; // 组ID
dev_t st_rdev; // (设备类型)若此文件为设备文件,则为其设备编号
off_t st_size; // 文件字节数(文件大小) --> 常用
blksize_t st_blksize; // 块大小(文件系统的I/O 缓冲区大小)
blkcnt_t st_blocks; // block的块数
time_t st_atime; // 最后一次访问时间
time_t st_mtime; // 最后一次修改时间(文件内容)
time_t st_ctime; // 最后一次改变时间(指属性)
};
2.3 判断文件类型和权限:
- 文件的类型和权限保存在struct stat结构体的st_mode中。
- 首先,我们需要使用stat函数来读取指定文件的属性,并保存至辅助的结构体变量中。
- 其次,调用一些宏来判断文件类型。man手册的stat的例子就给出了编写方法。
- 最后,通过位运算&来获取文件权限。详见文件的属性信息 | 爱编程的大丙 (subingwen.cn)
//这里不做健壮性判断
struct stat buf; //辅助结构体变量
stat(argv[1], &buf); //读取2.txt文件属性,保存至buf
//判断文件类型
if (S_ISREG(buf.st_mode)) {
printf("%s是常规文件\n", argv[1]);
}
//获取拥有者的权限
if (buf.st_mode & S_IRUSR) {
printf("r");
}
if (buf.st_mode & S_IWUSR) {
printf("w");
}
if (buf.st_mode & S_IXUSR) {
printf("x");
}
四、目录文件操作
目录文件操作类似上述的文件操作:首先,调用opendir函数打开目录,获得DIR指针;其次,将DIR指针传入readdir函数读取目录中每个文件的信息;最后,调用closedir函数关闭目录。
1. opendir函数
【1】头文件:#include <sys/types.h>、#include <dirent.h>
【2】函数原型:DIR *opendir(const char *name);
【3】返回值:成功返回文件流指针,失败返回NULL
【4】功能:打开目录name对应的文件流,该流被定位在目录中的第一个条目
2. readdir函数
【1】头文件:#include <dirent.h>
【2】函数原型:struct dirent *readdir(DIR *dirp);
【3】返回值:成功返回读到的文件的信息,目录文件被读完了或者函数调用失败返回 NULL
【4】struct dirent:
//来源: 爱编程的大丙
struct dirent {
ino_t d_ino; /* 文件对应的inode编号, 定位文件存储在磁盘的那个数据块上 */
off_t d_off; /* 文件在当前目录中的偏移量 */
unsigned short d_reclen; /* 文件名字的实际长度 */
unsigned char d_type; /* 文件的类型, linux中有7中文件类型 */
char d_name[256]; /* 文件的名字 */
};
3. closedir函数
【1】头文件:#include <sys/types.h>、#include <dirent.h>
【2】函数原型:int closedir(DIR *dirp);
【3】返回值:成功返回0,失败返回-1
4. 遍历单层目录
#include <sys/types.h>
#include <dirent.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv)
{
//1.打开目录
DIR *dir = opendir("./");
if (!dir) {
perror("打开失败");
return -1;
}
//2.统计所有.mp3后缀的文件个数
int count = 0;
while(1) {
struct dirent *tmp = readdir(dir);
//NULL则退出循环
if (!tmp) {
break;
}
//判定普通文件是否以.mp3结尾
if (tmp->d_type == DT_REG) {
char *findex = strstr(tmp->d_name, ".mp3");
if (findex && *(findex+4)=='\0') {
count++;
printf("%s\n", tmp->d_name);
}
}
}
printf("共有%d个.mp3文件\n", count);
//3.关闭目录
closedir(dir);
return 0;
}
5. 遍历多层目录(递归)
#include <sys/types.h>
#include <dirent.h>
#include <stdio.h>
#include <string.h>
int getMP3Num(const char *path)
{
//1.打开目录
DIR *dir = opendir(path);
if (!dir)
return 0;
//2.遍历目录,统计.mp3个数
int count = 0;
struct dirent *tmp = NULL;
while(tmp = readdir(dir)) {
//跳过目录.和..
if (strcmp(tmp->d_name, ".") == 0 ||
strcmp(tmp->d_name, "..") == 0)
{
continue;
}
//若为普通文件,则判定是否为.mp3
if (tmp->d_type == DT_REG) {
char *p = strstr(tmp->d_name, ".mp3");
if (p && *(p+4)=='\0') {
printf("%s\n", tmp->d_name);
++count;
}
}
//若为目录,则递归统计
else if (tmp->d_type == DT_DIR) {
char newPath[1024];
sprintf(newPath, "%s/%s", path, tmp->d_name);
count += getMP3Num(newPath);
}
}
//3.关闭目录
closedir(dir);
return count;
}
int main(int argc, char **argv)
{
int countNum = getMP3Num("./");
printf("共有%d个.mp3文件\n", countNum);
return 0;
}
6. scandir函数
除了上述的三个函数外,scandir函数也能进行单层目录的遍历。
【1】头文件:#include <dirent.h>
【2】函数原型:int scandir(const char *dirp, struct dirent ***namelist,
int (*filter)(const struct dirent *),
int (*compar)(const struct dirent **, const struct dirent **));
【3】参数说明:
- drip:需要遍历的目录名
- namelist:获得所有的文件信息保存于此。可以将该参数认为是整个二维数组的地址。注意:用完后free空间。
- filter:函数指针,指向的函数就是回调函数,需要在自定义函数中指定如何过滤目录中的文件。注意:自定义的函数返回值为1代表满足条件,为0代表不满足条件。
- compar:函数指针, 对过滤得到的文件进行排序, 可以使用提供的两种排序方式,即①alphasort: 根据文件名进行排序;②versionsort: 根据版本进行排序。
【4】返回值:成功返回匹配成功的文件个数,失败返回-1
【5】示例代码:
#include <stdio.h>
#include <dirent.h>
#include <string.h>
#include <stdlib.h>
//文件过滤:后缀为.mp3的普通文件
int isMP3(const struct dirent * dir)
{
char *p = strstr(dir->d_name, ".mp3");
return (dir->d_type == DT_REG && p && *(p+4)=='\0') ? 1 : 0;
}
//主函数
int main(int argc, char **argv)
{
struct dirent **namelist = NULL;
int ret = scandir(argv[1], &namelist, isMP3, alphasort);
printf("共有%d个.mp3文件\n", ret);
for (int i = 0; i < ret; ++i) {
printf("%s\n", namelist[i]->d_name);
free(namelist[i]);
}
free(namelist);
return 0;
}