参考韦东山老师教程:https://www.bilibili.com/video/BV1kk4y117Tu?p=12
目录
- 1. 文件IO函数分类
- 2. 函数原型
- 2.1 系统调用接口
- 2.2 标准IO接口
- 3. fileio内部机制
- 3.1 系统调用接口内部流程
- 3.1 dup函数使用
- 3.2 dup2函数使用
- 4. open file
- 4.1 open实例
- 4.2 open函数分析
- 5. create file
- 5.1 create实例
- 5.2 create分析
- 6. write file
- 6.1 write实例
- 6.2 write分析
- 7. read file
- 7.1 read实例
- 7.2 read分析
- 8.简单实例——处理.csv表格
1. 文件IO函数分类
在 Linux 上操作文件时,有两套函数:标准 IO、系统调用 IO。
标准 IO 的相关函数是:fopen/fread/fwrite/fseek/fflush/fclose 等 。
系统调用 IO 的相关函数是:open/read/write/lseek/fsync/close。
这 2 种 IO 函数的差别如下图所示:
2. 函数原型
2.1 系统调用接口
open/read/write/lseek/fsync/close 这几个函数的用法:
函数名 | 函数原型 | 描述 |
---|---|---|
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); | 作用:打开或者创建一个文件 pathnam:文件路径名,或者文件名 flags:表示打开文件所采用的操作 O_RDONLY:只读模式 O_WRONLY:只写模式 O_RDWR:可读可写 O_APPEND 表示追加,如果原来文件里面有内容,则这次写入会写在文件的最末尾 O_CREAT 表示如果指定文件不存在,则创建这个文件 O_EXCL 表示如果要创建的文件已存在,则出错,同时返回 -1,并且修改 errno 的值 O_TRUNC 表示截断,如果文件存在,并且以只写、读写方式打开,则将其长度截断为 0 O_NOCTTY 如果路径名指向终端设备,不要把这个设备用作控制终端 O_NONBLOCK 如果路径名指向 FIFO/块文件/字符文件,则把文件的打开和后继 I/O 设置为非阻塞模式(nonblocking mode) O_DSYNC 等待物理 I/O 结束后再 write。在不影响读取新写入的数据的前提下,不等待文件属性更新 O_RSYNC read 等待所有写入同一区域的写操作完成后再进行 O_SYNC 等待物理 I/O 结束后再 write,包括更新文件属性的I/O mode: 文件访问权限的初始值 |
read | #include <unistd.h> ssize_t read(int fd, void *buf, size_t count); | 作用:从给定的文件描述符指定的文件中,读取 count 个字节的数据,存放至 buf 中。 fd:指定要读写的文件描述符 buf:缓冲区,一般是一个数组,用于存放读取的内容 count:一次要读取的最大字节数 |
write | #include <unistd.h> ssize_t write(int fd, const void *buf, size_t count); | 作用:将 buf 中的 count 字节数据写入指定文件描述符的文件中 fd:指定要写入的文件描述符 buf:缓冲区,一般是一个数组,读取存放于该数组的内容存放于文件中 count:要写入的实际字节数 |
dup dup2 dup3 | #include <unistd.h> int dup(int oldfd); int dup2(int oldfd, int newfd); #define _GNU_SOURCE #include <fcntl.h> #include <unistd.h> int dup3(int oldfd, int newfd, int flags); | 作用:复制文件描述符,就是两个文件句柄指向同一个文件描述符,这两个文件句柄共享文件偏移地址、状态 oldfd:被复制的文件句柄 newfd:复制得到的文件句柄 注意:dup 函数返回的文件句柄是“未使用的最小文件句柄”,dup2 可以指定复制得到的文件句柄为 newfd,dup3 跟 dup2 类似,flags 参数必要么是 0,要么是 O_CLOEXEC |
lseek | #include <sys/types.h> #include <unistd.h> off_t lseek(int fd, off_t offset, int whence); | 作用:重新定位读/写文件偏移 fd:指定要偏移的文件描述符 offset:文件偏移量 whence:开始添加偏移 offset 的位置 SEEK_SET,offset 相对于文件开头进行偏移 SEEK_CUR,offset 相对文件当前位置进行偏移 SEEK_END,offset 相对于文件末尾进行偏移 |
fsync | #include <unistd.h> int fsync(int fd); | 作用:同步内存中所有已修改的文件数据到储存设备 fd:指定要同步的文件描述符 |
close | #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int close(int fd); | 作用:关闭已打开的文件 |
2.2 标准IO接口
fopen/fread/fwrite/fseek/fflush/fclose 这几个函数的用法:
函数名 | 函数原型 | 描述 |
---|---|---|
fopen | #include<stdio.h> FILE *fopen(const char *filename, const char *mode); | 作用:打开或者创建一个文件 filename:文件路径名,或者文件名 mode:文件的访问模式 r 打开一个用于读取的文件。该文件必须存在。 w 创建一个用于写入的空文件。如果文件名称与已存在的文件相同,则会删除已有文件的内容,文件被视为一个新的空文件。 a 追加到一个文件。写操作向文件末尾追加数据。如果文件不存在,则创建文件。 r+ 打开一个用于更新的文件,可读取也可写入。该文件必须存在。 w+ 创建一个用于读写的空文件。 a+ 打开一个用于读取和追加的文件。 |
fread | #include<stdio.h> size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); | 作用:从给定流 stream 读取数据到 ptr 所指向的数组中 ptr:指向带有最小尺寸 size*nmemb 字节的内存块的指针 size:这是要读取的每个元素的大小,以字节为单位 nmemb:元素的个数,每个元素的大小为 size 字节 stream:这是指向 FILE 对象的指针,该 FILE 对象指定了一个输入流 |
fwrite | #include <stdio.h> size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); | 作用:把 ptr 所指向的数组中的数据写入到给定流 stream 中 ptr:这是指向要被写入的元素数组的指针 size:这是要被写入的每个元素的大小,以字节为单位 nmemb:这是元素的个数,每个元素的大小为 size 字节 stream:这是指向 FILE 对象的指针,该 FILE 对象指定了一个输出流 |
fseek | #include <stdio.h> int fseek(FILE *stream, long int offset, int whence); | 作用:设置流 stream 的文件位置为给定的偏移 offset stream:这是指向 FILE 对象的指针,该 FILE 对象标识了流 offset:这是相对 whence 的偏移量,以字节为单位 whence:表示开始添加偏移 offset 的位置 SEEK_SET,offset 相对于文件开头进行偏移 SEEK_CUR,offset 相对文件当前位置进行偏移 SEEK_END,offset 相对于文件末尾进行偏移 |
fflush | #include <stdio.h> int fflush(FILE *stream); | 作用:刷新流 stream 的输出缓冲区 stream:这是指向 FILE 对象的指针,该 FILE 对象指定了一个缓冲流 |
fclose | #include <stdio.h> int fclose(FILE *stream); | 作用:关闭流 stream,且刷新其缓冲区 stream:这是指向 FILE 对象的指针,该 FILE 对象指定了要被关闭的流 |
3. fileio内部机制
3.1 系统调用接口内部流程
在Linux操作系统中,用户态(User Mode)和内核态(Kernel Mode)是两种不同的处理器运行模式,它们代表了程序执行时的不同权限级别和对系统资源访问的能力。
用户态(User Mode):
- 应用程序运行的正常状态。在用户态下,进程可以执行的指令受到限制,主要目的是保护系统资源和保持系统稳定性。应用程序不能直接访问硬件资源(如内存、CPU寄存器、I/O设备等)或者执行特权指令。
- 用户态下的进程运行在较低的CPU特权级别(通常是Ring 3),这意味着它们没有直接操作硬件的权限。
- 当应用程序需要执行某些特权操作(如读写文件、网络通信、分配内存等)时,它必须通过系统调用(system call)向操作系统内核发出请求,这时会从用户态转换到内核态。
内核态(Kernel Mode):
- 操作系统内核运行的状态,拥有最高权限。内核可以直接访问所有硬件资源,执行任意指令,并且可以改变处理器状态。
- 在内核态下,进程运行在最高的CPU特权级别(Ring 0),享有对所有系统资源的完全控制权。
- 内核态负责管理硬件资源、调度进程、处理中断和系统调用等核心功能。当系统调用发生时,处理器从用户态切换到内核态,执行内核代码完成请求的服务,然后返回用户态继续执行应用程序。
- 在内核态下,还有一种特殊的上下文称为“中断上下文”,这是当硬件中断发生时,CPU暂停当前任务,转而执行中断处理程序的状态。
简而言之,用户态保证了应用程序的安全隔离,而内核态则提供了对系统资源的直接访问和管理能力,两者之间的转换是操作系统管理和控制资源的关键机制。
fileio也是一样,系统调用接口open/read/write/lseek/fsync/close就是用户态应用程序能够调用的函数,这些接口是由GLIBC(GNU C Library)提供的,用来访问Linux的内核服务。
GLIBC提供的open/read/write/lseek/fsync/close函数访问Linux的步骤:
- 当使用open/read/write/lseek/fsync/close函数时,会设置异常原因,如open、read等,再调用汇编指令swi/svc来触发一个异常,并携带异常原因;
- CPU发现异常后会分辨异常原因,(假设是open函数)在sys_call_table[NR_open]中分辨异常原因后,会在fs/open.c文件中调用SYSCALL_DEFINE3即sys_open函数执行open文件操作。
其中
ABI(Application Binary Interface):应用二进制接口
OABI(Old ABI):旧的应用二进制接口
EABI(Embedded ABI):“E”代表“Embedded”,表示ARM嵌入式系统设计的一种新的ABI规范
sys_call_table的函数指针数组:
fs/open.c中SYSCALL_DEFINE3函数
3.1 dup函数使用
dup函数使用代码:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>int main(int argc, char **argv)
{char buf[10];char buf2[10];if (argc != 2){printf("Usage: %s <file>\n", argv[0]);return -1;}int fd = open(argv[1], O_RDONLY);int fd2 = open(argv[1], O_RDONLY);int fd3 = dup(fd);printf("fd = %d\n", fd);printf("fd2 = %d\n", fd2);printf("fd3 = %d\n", fd3);if (fd < 0 || fd2 < 0 || fd3 < 0){printf("can not open %s\n", argv[1]);return -1;}read(fd, buf, 1);read(fd2, buf2, 1);printf("data get from fd : %c\n", buf[0]);printf("data get from fd2: %c\n", buf2[0]);read(fd3, buf, 1);printf("data get from fd3: %c\n", buf[0]);return 0;
}
上传到Ubuntu后编译运行:
3.2 dup2函数使用
dup2函数使用代码:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>/** ./dup2 1.txt* argc = 2* argv[0] = "./dup2"* argv[1] = "1.txt"*/int main(int argc, char **argv)
{int fd;if (argc != 2){printf("Usage: %s <file>\n", argv[0]);return -1;}fd = open(argv[1], O_RDWR | O_CREAT | O_TRUNC, 0777);if (fd < 0){printf("can not open file %s\n", argv[1]);printf("errno = %d\n", errno);printf("err: %s\n", strerror(errno));perror("open");}else{printf("fd = %d\n", fd);}/* fd=1的文件是系统标准输出文件,如printf*/dup2(fd, 1);printf("hello, world\n"); /* 打印到fd=1的文件 */return 0;
}
上传到Ubuntu后编译运行:
4. open file
4.1 open实例
open代码:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>/** ./open 1.txt* argc = 2* argv[0] = "./open"* argv[1] = "1.txt"*/int main(int argc, char **argv)
{int fd;if (argc != 2){printf("Usage: %s <file>\n", argv[0]);return -1;}fd = open(argv[1], O_RDWR);if (fd < 0){printf("can not open file %s\n", argv[1]);printf("errno = %d\n", errno);printf("err: %s\n", strerror(errno));perror("open");}else{printf("fd = %d\n", fd);}while (1){sleep(10);}close(fd);return 0;
}
上传到Ubuntu后编译运行:
补充:error变量、strerror函数、perror函数
errno:errno h头文件定义了整数变量,它在系统调用和一些库函数在发生错误时被设置,以指示出错的地方。
在man errno中能够找到对应的错误码:
strerror:函数 char *strerror(int errnum); 指向errnum对应的字符串,使用 printf(“err: %s\n”, strerror(errno)); 就能打印出对应的错误码含义:
perror:函数 void perror(const char *s); 打印参数字符串s,后跟冒号和空白,然后显示一条与当前值oferrno相对应的错误消息和一行新行:
4.2 open函数分析
使用编写的open应用程序在后台分别打开2个文件,发现获得的文件句柄fd都是3,查看2个后台正在运行的程序进程,发现2个进程都有4个文件句柄,其中fd 0表示系统标准输入(stdin),例如scanf;fd 1表示系统标准输出(stdout),例如printf;fd 2表示标准错误(stderr),存放错误信息;fd 3是应用程序刚刚打开的文件句柄:
由此可知,先后2次使用open函数在后台打开文件,每次执行open时分配一个进程号,2次open属于不同的进程,所以2次open文件返回的文件句柄都是3并不冲突。
在open的执行过程中,文件句柄fd是如何与进程号关联起来的?需要根据代码进行分析:
由3.1章节可知,调用open函数时最终调用到fs/open.c中do_sys_open内核函数来打开文件:
继续分析fd_install函数:
其中current结构体为什么是task_struct结构体还需要后续再分析。
查看task_struct结构体,在task_struct结构体中有files_struct结构体:
files_struct结构体中会有一个fdtab:
fdtable结构体中会有file结构体,保存文件句柄fd:
因此对于每次执行的open文件操作都会创建一个task_struct结构体,在对应的fdtable中会保存此进程打开的文件句柄fd:
在fdtable的file结构体中会有一个f_pos保存当前偏移位置,在调用lseek、read、write 都会更新f_pos的位置:
5. create file
5.1 create实例
create代码:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>/** ./create 1.txt* argc = 2* argv[0] = "./open"* argv[1] = "1.txt"*/int main(int argc, char **argv)
{int fd;if (argc != 2){printf("Usage: %s <file>\n", argv[0]);return -1;}fd = open(argv[1], O_RDWR | O_CREAT | O_TRUNC, 0777);if (fd < 0){printf("can not open file %s\n", argv[1]);printf("errno = %d\n", errno);printf("err: %s\n", strerror(errno));perror("open");}else{printf("fd = %d\n", fd);}while (1){sleep(10);}close(fd);return 0;
}
上传到Ubuntu后编译运行:
5.2 create分析
“open(argv[1], O_RDWR | O_CREAT | O_TRUNC, 0777);”参考2.1章节系统调用接口中open描述,第2个参数是以可读可写、若文件不存在则创建文件、截取文件的方式open文件,第3个参数是open的文件权限是777,但是创建的文件2.txt权限并不是777,而是775,原因是系统的umask是2,所以其他用户的写权限是默认被关闭的:
create是使用open函数实现的,在do_sys_open中调用build_open_flags打开文件是会使用到flags和mode参数:
操作与open函数基本相同。
6. write file
6.1 write实例
write代码:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>/** ./write 1.txt str1 str2* argc = 2* argv[0] = "./open"* argv[1] = "1.txt"*/int main(int argc, char **argv)
{int fd;int i;int len;if (argc < 3){printf("Usage: %s <file> <string1> <string2> ...\n", argv[0]);return -1;}fd = open(argv[1], O_RDWR | O_CREAT | O_TRUNC, 0644);if (fd < 0){printf("can not open file %s\n", argv[1]);printf("errno = %d\n", errno);printf("err: %s\n", strerror(errno));perror("open");}else{printf("fd = %d\n", fd);}for (i = 2; i < argc; i++){len = write(fd, argv[i], strlen(argv[i]));if (len != strlen(argv[i])){perror("write");break;}write(fd, "\r\n", 2);}close(fd);return 0;
}
上传到Ubuntu后编译运行:
write_in_pos代码:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>/** ./write 1.txt str1 str2* argc = 2* argv[0] = "./open"* argv[1] = "1.txt"*/int main(int argc, char **argv)
{int fd;int i;int len;if (argc != 2){printf("Usage: %s <file>\n", argv[0]);return -1;}fd = open(argv[1], O_RDWR | O_CREAT, 0644);if (fd < 0){printf("can not open file %s\n", argv[1]);printf("errno = %d\n", errno);printf("err: %s\n", strerror(errno));perror("open");}else{printf("fd = %d\n", fd);}printf("lseek to offset 3 from file head\n");lseek(fd, 3, SEEK_SET);write(fd, "123", 3);close(fd);return 0;
}
上传到Ubuntu后编译运行:
6.2 write分析
搜索SYSCALL_DEFINE3调用write的函数:
分析SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t, count)函数过程:
7. read file
7.1 read实例
read代码:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>/** ./read 1.txt* argc = 2* argv[0] = "./read"* argv[1] = "1.txt"*/int main(int argc, char **argv)
{int fd;int i;int len;unsigned char buf[100];if (argc != 2){printf("Usage: %s <file>\n", argv[0]);return -1;}fd = open(argv[1], O_RDONLY);if (fd < 0){printf("can not open file %s\n", argv[1]);printf("errno = %d\n", errno);printf("err: %s\n", strerror(errno));perror("open");}else{printf("fd = %d\n", fd);}/* 读文件/打印 */while (1){len = read(fd, buf, sizeof(buf)-1);if (len < 0){perror("read");close(fd);return -1;}else if (len == 0){break;}else{/* buf[0], buf[1], ..., buf[len-1] 含有读出的数据* buf[len] = '\0'*/buf[len] = '\0';printf("%s", buf);}}close(fd);return 0;
}
上传到Ubuntu后编译运行:
7.2 read分析
搜索SYSCALL_DEFINE3调用read的函数:
分析SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t, count)函数过程:
8.简单实例——处理.csv表格
背景描述:创建一个score.csv表格,文件中包含张三、李四、王五同学的语文、数学、英语分数。
处理步骤:
- 打开score.csv表格
- 通过score.csv表格中的分数计算出每位同学的总分并对分数进行评价,最终结果输出到指定的表格文件(result.csv)中(评价标准:3科总分>=270为A+等级、3科总分>=240为A等级、其余成绩为B等级)
补充:使用notepad++打开score.csv表格,每一列之间用’,'隔开,每一行之间用回车换行隔开
处理score.csv表格数据时需要先读取score.csv表格中的数据到一个buf中,此时可以用回车换(0x0d和0x0a)作为读取每一行数据的结束标志,参考函数read_line()。
代码:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>/* 返回值: n表示读到了一行数据的数据个数(n >= 0)* -1(读到文件尾部或者出错)*/
static int read_line(int fd, unsigned char *buf)
{/* 循环读入一个字符 *//* 如何判断已经读完一行? 读到0x0d, 0x0a */unsigned char c;int len;int i = 0;int err = 0;while (1){len = read(fd, &c, 1);if (len <= 0){err = -1;break;}else{if (c != '\n' && c != '\r'){buf[i] = c;i++;}else{/* 碰到回车换行 */err = 0;break;}}}buf[i] = '\0';if (err && (i == 0)){/* 读到文件尾部了并且一个数据都没有读进来 */return -1;}else{return i;}
}void process_data(unsigned char *data_buf, unsigned char *result_buf)
{/* 示例1: data_buf = ",语文,数学,英语,总分,评价" * result_buf = ",语文,数学,英语,总分,评价" * 示例2: data_buf = "张三,90,91,92,," * result_buf = "张三,90,91,92,273,A+" **/char name[100];int scores[3];int sum;char *levels[] = {"A+", "A", "B"};int level;if (data_buf[0] == 0xef) /* 对于UTF-8编码的文件,它的前3个字符是0xef 0xbb 0xbf */{strcpy(result_buf, data_buf);}else{sscanf(data_buf, "%[^,],%d,%d,%d,", name, &scores[0], &scores[1], &scores[2]);//printf("result: %s,%d,%d,%d\n\r", name, scores[0], scores[1], scores[2]);//printf("result: %s --->get name---> %s\n\r", data_buf, name);sum = scores[0] + scores[1] + scores[2];if (sum >= 270)level = 0;else if (sum >= 240)level = 1;elselevel = 2;sprintf(result_buf, "%s,%d,%d,%d,%d,%s", name, scores[0], scores[1], scores[2], sum, levels[level]);//printf("result: %s", result_buf);}
}/** ./process_excel data.csv result.csv* argc = 3* argv[0] = "./process_excel"* argv[1] = "data.csv"* argv[2] = "result.csv"*/int main(int argc, char **argv)
{int fd_data, fd_result;int i;int len;unsigned char data_buf[1000];unsigned char result_buf[1000];if (argc != 3){printf("Usage: %s <data csv file> <result csv file>\n", argv[0]);return -1;}fd_data = open(argv[1], O_RDONLY);if (fd_data < 0){printf("can not open file %s\n", argv[1]);perror("open");return -1;}else{printf("data file fd = %d\n", fd_data);}fd_result = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0644);if (fd_result < 0){printf("can not create file %s\n", argv[2]);perror("create");return -1;}else{printf("resultfile fd = %d\n", fd_result);}while (1){/* 从数据文件里读取1行 */len = read_line(fd_data, data_buf);if (len == -1){break;}//if (len != 0)// printf("line: %s\n\r", data_buf);if (len != 0){/* 处理数据 */process_data(data_buf, result_buf);/* 写入结果文件 *///write_data(fd_result, result_buf);write(fd_result, result_buf, strlen(result_buf));write(fd_result, "\r\n", 2);}}close(fd_data);close(fd_result);return 0;
}
代码及score.csv文件上传到ubuntu后运行效果:
将result.csv文件传回到windows打开: