第一节:文件IO与文件描述符
为什么要有两套IO?
- 标准IO=连锁酒店前台,你只管说给我一间房,细节前台帮你办
- 文件IO=你自己拿钥匙开门、收拾房间,一点弯路都不走,但什么都要自己干
文件描述符(fd)是什么?
fd就是一个非负整数,是内核给你的号码牌
| fd | 名字 | 是什么 |
|---|---|---|
| 0 | stdin | 标准输入(键盘) |
| 1 | stdout | 标准输出(屏幕) |
| 2 | stderr | 标准错误(报错) |
| 这三个是shell默认给你打开的。所以open第一个新文件,拿到的fd一定是3 |
fd不只是文件
内核用以标识一个特定进程正在访问的文件,其他资源的访问标识,fd是一切资源的统一编号
为什么标准IO能移植?
因为每个操作系统都各自实现了一套C标准库。你在windows上写的fopen,背后是windows自己的实现,不是Linux的,所以移植的是源码,不是二进制
为什么文件IO不能移植?
因为open这个函数只有Linux内核才有,
🔴写open/read/write必须包含<fcntl.h>和<unistd.h>
第二节:open与它的flags
🟠函数原型
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
mode(权限)只有在O_CREAT(创建新文件)时才用得上,如果文件已经存在、你也只是读它,权限根本没意义
返回值:
- 成功:返回fd(非负整数,第一个新文件必然是3)
- 失败:返回-1,并设置errno
**第一组flags:读写方式(三个值互斥)
| flag | 名字 | 效果 |
|---|---|---|
| O_RDONLY | 只读 | 我只能读它(不能写) |
| O_WRONLY | 只写 | 我只能写它(不能读 |
| O_RDWR | 读写 | 都可以 |
| 比如: |
int fd = open("a.txt",O_RDONLY);
int fd = open("a.txt",O_RDONLY | O_WRONLY); //错误,互斥,不能同时选
O_RDONLY在Linux里面等于0
#define O_RDONLY 0 //只读
#define O_WRONLY 1 //只写
#define O_RDWR //读写
因为flags参数是一个整数,内核用位运算来检查每一位
🟡flags的附加行为
| flag | 名字 | 效果 | 对应fopen模式 |
|---|---|---|---|
| O_CREAT | 创建 | 文件不存在就创建新的 | w a |
| O_EXCL | 独占 | 配合O_CREAT:文件已经存在就报错 | 无 |
| O_TRUNC | 截取 | 文件已存在,把原内容清空 | w |
| O_APPEND | 追加 | 所有写操作强制在文件末尾 | a |
第三个参数mode:只有O_CREAT才有效
int open(const char *pathname, int flags, mode_t mode);
mode是新创建文件的权限,用8进制表示:
| 八进制 | 二进制 | 含义 |
|---|---|---|
| 6 | 110 | 读写 |
| 4 | 100 | 只读 |
| 7 | 111 | 读写执行 |
对照表:fopen模式=== open flags
| fopen模式 | 对应的open写法 |
|---|---|
| r | open(path,O_RDONLY) |
| r+ | open(path,O_RDWR) |
| w | open(path,O_WRONLY | O_CREAT | O_TRUNC, 0664) |
| w+ | open(path,O_RDWR | O_CREAT | O_TRUNC, 0664) |
| a | open(path, O_WRONLY | O_CREAT | O_APPEND, 0664) |
| a+ | open(path, O_RDWR | O_CREAT | O_APPEND, 0664) |
close关闭文件
int close(int fd);
- 成功:返回0
- 失败:返回-1并设置errno
第三节:read与write
函数原型
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf,size_t count);
三个参数,含义完全一样,只是方向不同:
| 参数 | 读 | 写 |
|---|---|---|
| fd | 从哪个fd读 | 往哪个fd写 |
| buf | 数据读到这里 | 数据从这里写出去 |
| count | 最多读多少字节 | 最多写多少字节 |
返回值
ssize_t n = read(fd,buf,sizeof(buf));
| 返回值 | 含义 |
|---|---|
| >0 | 实际读写的字节数 |
| =0 | 读:读到文件末尾/写:什么都没写进去 |
| =-1 | 出错了,设置errno |
| 🔴关键:read的返回值永远不超过count | |
| read是同步调用,读完才返回,会一直读到读满1024或文件到末尾才停。最多只能给你实际有点哪些 |
read为什么是阻塞的
n = read(fd,buf,1024);
如果文件里面只有500字节,这一行会立刻返回500吗?会
但是如果fd是个"管道"或者"socket",那就不一定,read会一直等,等到有数据来才返回,这就是阻塞
所以read的行为分为两种:
| fd类型 | read的行为 |
|---|---|
| 普通文件 | 到末尾就返回,不阻塞 |
| 管道、socket、终端 | 阻塞,等有数据、有连接、有输入才返回 |
read/write的一个副作用:会移动文件的位置
read之后,光标自动往前移
lseek(fd,0,SEEK_END); //光标顶到末尾
printf("%ld\n",lseek(fd,0,SEEK_CUR)); //打印文件的大小
ssize_t n = read(fd,buf,100); //读最后100字节
printf("%ld\n",lseek(fd,0,SEEK_CUR)); //位置变了
第四节:lseek---一个函数干两件事
第一章的定位方法是分两步:
fseek(fp,576,SEEK_SET); //第一步:挪位置
long pos = ftell(fp); //第二步:问位置
lseek把这两件事合并成了一个原子操作:
off_t pos = lseek(fd,576,SEEK_SET); //挪完之后,直接告诉你挪到哪里
大白话:fseek像你自己走过去,ftell像你问自己在哪;lseek像你跟司机说送我到22号,司机顺口告诉你到了22号
函数原型:
off_t lseek(int fd, off_t offset, int whence);
三个参数和fseek一模一样,返回值:
| 函数 | 成功返回 | 失败返回 |
|---|---|---|
| fseek | 0 | -1 |
| lseek | 新的文件位置 | -1 |
| 所以正确的写法: |
off_t pos = lseek(fd,576,SEEK_SET);
if(pos == (off_t)-1){ //必须和-1比较,而不是0,因为只有刚好定位到0字节才会返回0
perror("lseek");
exit(-1);
}
printf("定位成功\n");
🔴这里为什么要写(off_t)-1,而不是直接写-1?因为严格来说off_t可能有符号有可能没有符号
三个whence
| whence | 基准点 | lseek(fd,0,X)的效果 |
|---|---|---|
| SEEK_SET | 文件开头 | 跳到开头,返回0 |
| SEEK_CUR | 当前位置 | 原地不动,返回当前位置 |
| SEEK_END | 文件末尾 | 跳到末尾,返回文件大小 |
第五节:fdopen---打通两套体系
fdopen的作用:一个补办手续的函数
问题的场景:我用open拿到了fd,但我现在想用fgets、fprintf这些好用的标准IO函数
FILE *fdopen(int fd,const *mode);
- 成功:返回一个FILE *(这个背后装的就是fd)
- 失败:返回NULL并设置errno:fdopen不会复制内容,他只是给已有的fd套上一个FILE结构体
mode参数写什么?mode必须和open时用的flags兼容
为什么不能随便写:因为FILE结构体里记着我现在能读还是写,如果open时,只申请了只读权限,fdopen却告诉库函数他能写,那么真写的时候就会报错
只能单向,没有openfp
常规是把文件IO转成标准IO
因为fopen内部已经做好了一个完整的FILE结构体,里面还包着一个fd,完全可以直接访问这个fd
因为fopen虽然操作fd,但它返回的是FILE*,属于标准IO家庭,所以归stdio.h管