课程内容#
文件操作#
- 【引入】之前学过的 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] 不需要特意记头文件
- 描述
- 系统调用 [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 中
- 描述 + 返回值
- 尝试读取最多 count 字节到 buffer 中
- 读取字节数达不到 count 的情况:被人中断 [signal];数据本身不足 count 大小
- 每成功读取 num [≤ count] 字节数据,文件偏移量 [像指针] 会自动往后走 num 大小
- 如果文件偏移量在 EOF [没有数据可读了],函数返回 0
- count
- 如果设为 0,错误可能被检测出来,如果没有检测到错误,返回 0
- 如果大于 SSIZE_MAX [int /long int 的最大值],返回的结果将是定义好的 [POSIX.1 标准]
- 返回值
- ≤ count
- 出错时返回 - 1,并会设置 errno
- [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
- 描述
- 关联一个流 [stream]
- [PS] 网络上发布数据,字节流;文件流 < 类型:FILE *>
- mode
- r /r+:读 / 读写
- 流在文件开始
- w /w+:读 / 读写
- 流在文件开始
- 文件存在,截断文件 [打开就会将原数据清除]
- 文件不存在,创建文件
- a /a+:追加 / 读与追加
- 追加时,流在 EOF;读时,流在文件开始
- 文件不存在就会创建
- +:读写通吃
- [PS]
- b:可以在 mode 字符串的最后或者两个字符之间,可用于处理二进制文件,但在 Linux 上一般没有效果
- ❓ 任何被创建的文件会被进程的 umask value 修正
- r /r+:读 / 读写
- 返回值
- 成功时,返回文件指针
- 出错时,返回 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