课程内容
文件操作
- 【引入】之前学过的cp、mv、cat命令,都涉及到文件的读写
- cp:读→写
- mv:读→写→删
- cat:读→写
- 上面这些步骤是如何实现的呢?
【底层操作,基于文件描述符】
open
打开或创建一个文件 [别名:openat、create]
- man 2 open【关注函数原型及描述】
- 原型
- 返回值 int:文件描述符 或 -1
- 常用文件描述符:0-stdin、1-stdout、2-stderr
- -1:发生错误,并会设置errno [可供perror使用,见代码演示]
- flags:文件的打开方式
- [PS] 不需要特意记头文件
- 返回值 int:文件描述符 或 -1
- 描述
- 系统调用 [system call]:帮助你做你没有权限做的事情
- 如果open的文件不存在,可能会新建该文件 [当flags里定义了O_CREAT]
- O_CREAT
- open函数的flag
- C语言体系中,全大写表示宏定义
- 底层是一个int型数据,叫做位掩码
- 32位,可表示32种状态,每一位表示一种状态
- 状态之间可使用与、或、异或的方式转换
- O_CREAT
- 文件描述符 [file descriptor]
- 小、非负、后续系统可调用 [read、write...]
- 返回值永远是当前进程中可以取的最小的数字
- 可用来判断文件数量 [如果返回1000,当前文件数量一定超过1000了]
- 打开文件后,文件指针默认在文件头部
- 文件描述 [file description]
- 每次调用open会创建一个新的open文件描述,它是系统全局文件表中的一条信息
- 记录文件偏移量和文件的状态
- [PS] 文件描述符是一个open文件描述的引用,不因pathname的改变而受影响
- 每次调用open会创建一个新的open文件描述,它是系统全局文件表中的一条信息
- ⭐flags
- 必须包含O_RDONLY、O_WRONLY、O_RDWR中的一个
- flag之间用位或组合
- O_CREAT 创建
- O_TRUNC 截断
- O_DIRECT 直接IO
- 直接IO——同步写,文件会直接写进去,而不经过缓冲
- 缓冲IO
- 缓冲结束条件:① 攒到一堆数据;② 等固定时间
- 往磁盘写一个字符a,不会马上写入磁盘,可以减小成本
- 但断电时易丢失数据
- [PS]
- 磁盘的最小单元是块,每个块是4K
- 所以磁盘又叫块设备
- 类似printf输出到stdout的条件 [行缓冲]
- 遇到回车 / 程序结束,系统自动冲洗缓冲区
- 缓冲区满,自动冲洗
- fflush函数,手动冲洗
- 磁盘的最小单元是块,每个块是4K
- 缓冲结束条件:① 攒到一堆数据;② 等固定时间
- O_NONBLOCK 非阻塞IO
- 阻塞
- 如:scanf的时候,要等标准输入流中有输入才能进行后面的操作
- 缺点:浪费资源
- 非阻塞
- 不会等
- 缺点
- 需频繁回来查看,也浪费资源
- 需有某种机制监测,花费技术成本
- 阻塞
- O_TMPFILE 创建临时文件
- 进程运行结束后文件就会被删除,交易关闭也会
- 类似系统的临时文件夹/tmp
- ❗ 底层读写文件前,需要调用open函数获得文件描述符
read
通过文件描述符读取数据
- man read
- 原型
- 返回值 ssize_t:读取的字节数 或 -1
- 以_t结尾,一般是用户自定义类型
- 猜测:也是基本类型之一,可能是long long,可能是int
- 通过ctags一步步找具体类型:ctrl + ] 、ctrl + o
- 答案:int [32位系统下];long int [64位系统下]
- [PS] 按理说,32位系统下,long int大小等同于int
- buf、count:每次最多读取count字节数据到buf中
- 返回值 ssize_t:读取的字节数 或 -1
- 描述 + 返回值
- 尝试读取最多count字节到buffer中
- 读取字节数达不到count的情况:被人中断 [signal];数据本身不足count大小
- 每成功读取num [≤ count] 字节数据,文件偏移量 [像指针] 会自动往后走num大小
- 如果文件偏移量在EOF [没有数据可读了],函数返回0
- count
- 如果设为0,错误可能被检测出来,如果没有检测到错误,返回0
- 如果大于SSIZE_MAX [int / long int的最大值],返回的结果将是定义好的 [POSIX.1标准]
- 返回值
- ≤ count
- 出错时返回-1,并会设置errno
- 尝试读取最多count字节到buffer中
- [PS] ERRORS
- EAGAIN
- 读文件 [包括socket] 的时候,尽管文件已被设置为O_NONBLOCK,read将会阻塞
- EAGAIN
write
通过文件描述符写数据
- man 2 write
- 原型
- 与read很相似
- 描述 + 返回值
- 与read很相似
- 写的字节数达不到count的情况:物理空间不够;系统资源限制;被signal中断
- open文件时设置了O_APPEND [追加]
- 文件偏移量 [offset] 在文件末尾,写操作会追加
- 否则放在开头,写操作会覆写
close
关闭一个文件描述符
- man close
- 主要就是关闭文件描述符
- [PS]
- 记录锁将被移除
- 特殊情况
- close的是文件描述的最后一个文件描述符,文件描述对应的资源会被释放
- close的是文件的最后一个引用的文件描述符,文件将会被删除
- 暂时不用在意内核具体做了什么
【标准文件操作,基于文件指针】
<stdio.h>
fopen
通过流打开文件
- man fopen
- 原型
- 返回值 FILE *:文件指针
- 早期是宏定义,这里大写是为了兼容性
- mode
- 类型是char *,而不是int
- 返回值 FILE *:文件指针
- 描述
- 关联一个流 [stream]
- [PS] 网络上发布数据,字节流;文件流 <类型:FILE *>
- mode
- r / r+:读 / 读写
- 流在文件开始
- w / w+:读 / 读写
- 流在文件开始
- 文件存在,截断文件 [打开就会将原数据清除]
- 文件不存在,创建文件
- a / a+:追加 / 读与追加
- 追加时,流在EOF;读时,流在文件开始
- 文件不存在就会创建
- +:读写通吃
- [PS]
- b:可以在mode字符串的最后或者两个字符之间,可用于处理二进制文件,但在Linux上一般没有效果
- ❓ 任何被创建的文件会被进程的umask value修正
- r / r+:读 / 读写
- 关联一个流 [stream]
- 返回值
- 成功时,返回文件指针
- 出错时,返回NULL,并设置errno
fread、fwrite
二进制流的IO
- man fread / fwrite
- fread:从stream中读nmeb次数据 [size字节 / 次] 到ptr
- fwrite:把ptr的数据写nmeb次数据 [size字节 / 次] 到stream
- 返回值 size_t:读 / 写的items数量 [成功]
- [无符号的ssize_t]
- 发生错误或提前遇到EOF时 👉 0 ≤ 返回值 < nmeb
- ❗ 所以不能通过返回值区分EOF和错误,需使用feof、ferror确认
- [PS] 当size为1时,返回值等于传输的字节数
fclose
刷新流并关闭文件描述符
- man fclose
- 刷新流实际调用的是fflush
- 返回值
- 0 [成功]
- -1(EOF),并设置errno [失败]
- 未定义行为 [传入的是一个非法指针或已经被fclose过]
- ⭐标准IO里的所有操作都是缓冲IO
- 本身没有权限写,需等待内核控制
- ❓ 标准IO更适合文本 [用户],底层的IO更适合二进制文件
目录操作
本质上也是文件 [早期可以直接open]
opendir
- man opendir
- 返回值 DIR *:目录流指针 或 NULL
- 目录流默认被放置在目录的第一个条目
- 发生错误时,返回NULL,并设置errno
readdir
- man readdir
- 返回值 struct dirent *:目录项 或 NULL
- 目录流中下一个目录项 [结构体] 的指针
- 结构体的主要字段:d_ino、d_name
- [PS]
- 一次一次地返回下一个文件
- d_off:与telldir返回的值一样,又类似ftell()
- 此offset [每个文件的大小不同] 与一般意义 [字节为单位] 上的不一样
- ftell()获取当前文件所在位置指示器的值
- NULL [遇到目录流结尾或发生错误时]
- 目录流中下一个目录项 [结构体] 的指针
closedir
- 关闭目录
实现ls -al的基本思路
- ls -al效果
- 需要的信息有:文件权限、连接数、用户名、组名、文件大小、修改时间、文件名
- 思路
- readdir()
- man readdir
- 读取目录里的每一个文件
- 可获取文件名
- stat()、lstat()
- man 2 stat
- 根据文件路径获取文件信息:stat结构体
- 可获取文件权限、硬连接数、uid、gid、文件大小、修改时间
- 可参考里面的EXAMPLE:lstat
- lstat()和stat()的区别
- lstat()可以查看软连接的信息,而不会跳到软连接指向的文件
- getpwuid()
- man getpwuid
- 根据uid获取passwd结构体
- 可获取对应的用户名
- getgrgid
- man getgrgid
- 根据gid获取group结构体
- 可获取对应的组名
- 如果自己去实现 → 读文件、切分
- 用户信息:/etc/passwd
- 组的信息:/etc/group
- readdir()
- 其他细节
- 颜色
- 排序
- 纯ls命令输出的显示列数随宽度变化
- 获取终端大小
- 参考ioctl,man ioctl
- 列宽如何确定,可用暴力方式、二分查找、慢慢逼近
代码演示
底层文件操作
- ⭐ 详见注释,关注使用
- ❗ 避免乱码情况
- 字符串buff末尾留一位放'\0'
- sizeof(buff) - 1
- 最后一次不足512字节的读取,需要排除多余字节的干扰
- 方法①:手动menset(buff, 0, sizeof(buff))
- 方法②:始终保持数据末尾是'\0',buff[nread] = '\0'
- [PS] 学习系统上层命令时,不必太关注这些
- 字符串buff末尾留一位放'\0'
- perror 打印一个系统错误信息
- man 3 perror
- 原型
- fopen等发生错误时就会设置errno
- 描述
- 在stderr上输出上一个调用的错误信息
- s通常包含函数的名称
- 建一个常用的common头文件夹,放常用的头文件
- head.h
标准文件操作
- buffer放在循环里,每次都会初始化
- nread为非负数,并且不能区分EOF和错误
标准IO是缓冲IO
- 第一个"Hello world"直接输出,stderr没有缓冲
- 第二个"Hello world"本来会等sleep结束,无法输出到stdout,但是可以立马输出,通过👇
- 手动刷新缓冲区:fflush
- 输出换行
- sleep函数在unistd.h中
附加知识点
- ulimit -a,可查看可打开的文件数量上限
- 每个进程中文件打开数量上限为1024
- 超过会使系统崩盘
- [PS]
- 系统崩盘还需考虑内存
- 要做一个负责任的程序:手动close / free、输出错误日志
- 只有标准输出是行缓冲的
思考点
- ❓ 文件保存就会立即写入磁盘吗?
- 参考Are file edits in Linux directly saved into disk?——StackExchange
Tips
- 在vim中,Shift + K可跳到man手册
- 推荐复制即翻译软件:CopyTranslator
- man手册在线文档:man page——die.net