文件IO
目标
1 . 理解文件描述符
2 . 基础文件IO函数(open creat lseek close read write )
3 . 进阶文件IO函数(dup fcntl sync)
点击查看代码
1. 文件描述符
非负整数,open 或creat时返回的文件描述符,必定为最小的可用描述符
基础文件IO
当open 打开文件时,设置了O_APPEND标识,即使使用lseek改变文件偏移,写入依然会追加在文件末尾,并且文件偏移为写入前文件长度 + 写入长度。
O_APPEND,似乎会将lseek(fd, 0 , 2 ) 和 write 绑定成一个原子操作,需要验证
od -c , -c表示以字节方式, od命令查看文件的原始内容
ssize_t read (int fd, void* buff, szie_t nbyte)
ret:成功返回读取的实际字节数,失败返回-1
arg :nbyte,读取的nbyte字节数;
不会自动在buff最后添加\0 ;
文件结尾再调用read 返回0 ;
实际读取字节数小于nbyte的情况:
文件末尾,不足nbyte
fd对应终端设备,一次读入一行
从网络中读,网络缓冲可能小于nbyte
fd对应管道和fifo,如果管道包含字节小于nbyte
面向记录的设备,一次返回一条记录,小于nbyte
read 中途由于信号造成中断,此时,虽然读取的数据没有nbyte,但是指针还是会偏移到开始时的nbyte之后,(lseek,后退,重读)
一次可读入的最大字节数为,SSIZE_MAX
ssize_t write (int fd, void *buff, size_t nbyte)
ret: 实际写入字数,失败返回-1 ;
通常write 不会失败,返回nbyte。 失败的原因可能是磁盘满了或者超出文件长度限制
ssize_t lseek(fd, offset, whence)
ret:当前文件偏移位置,失败返回-1
arg :whence可以有三个宏SEEK_SET,SEEK_CUR,SEEK_END,对应0 1 2
分别表示从文件开头,文件当前位置,文件结尾处。
强调:SEEK_END,是文件最后一个字符的下一个位置,和c++容器中end ()一样
buffer设置大小和文件IO效率
在需要读取字节数非常多的情况下
当buffer设置很小,甚至是1 时,效率会非常低
随着buffer增大,读取耗时会显著降低,
当buffer超过128 byte 时(实验场景),随着buffer增大,耗时几乎不变,这是由于文件系统的预读(read ahead)机制造成的;
当检测到正在顺序读取时,系统试图读入更多的数据并假想程序会很快读取这些数据。此时,增大buffer几乎不会影响读取效率
随着buffer继续增加,效率反而会有所降低,这是因为buffer大小比预读空间还要大,超过预读空间的部分没有收益于预读的影响
内核如何记录打开的文件:
1. 进程表中记录着 打开文件描述符表 (每个表项包含:文件描述符标志, 指向文件表某一表项指针)
2. 所有打开文件共有一个文件表,(每个表项包含:文件状态(读,写,非阻塞...), 当前文件偏移, 指向文件v-node的指针)
3. 每个打卡文件(设备)有一个 v-node结构。(包含:文件类型,对此种文件进行各种操作的函数指针, 大多文件都有的i-node)
linux系统,只有i-node,没有v-node
每个进程私有的打开文件描述符表
区别文件描述符标识, 文件状态标识, 前者是作用于进程的一个文件描述符,后者作用于所有指向该文件表项的文件描述符
可能出现多个文件描述符指向同一个文件表项,比如 dup,fork
保证多进程对同一文件原子性的写入:
使用 pread 和 pwrite,(fd, buff, nbyte, offset);用法和read ,write 几乎一样,相当于将lseek,和read ,write 绑定成原子操作
使用 pread 和 pwrite 时无法中断,并且在函数结束后才更行文件偏移
文件IO进阶
文件描述符复制
int dup (int fd);
ret: 成功返回新文件描述符(当前最小可用文件描述符),失败返回-1
int dup2 (int fd, int fd2);
ret:成功返回新文件描述符(fd2指定的文件描述符),失败返回-1
如果fd2 已经有打开文件,则先将其关闭; 如果fd2 等于fd1,则直接返回fd
dup(fd) 等效于 fcntl (fd, F_DUPFFD, 0 );
dup2(fd, fd2) 几乎等效于 close (fd2); fcntl (fd, F_DUPFD, fd2); 不同之处在于dup2是原子操作
新描述符独占一个打开文件描述符表项, 和被复制描述符公用一个文件表项(文件标志,偏移,v-node相同)
保证文件一致性(清空缓存)
大多数磁盘IO都通过缓冲进行,当需要写入文件时,先将数据拷贝进缓冲区,当缓冲满或者需要重用缓冲时,才将缓冲排入输出队列,等其到达队首时,才进行实际IO,这种方式称为延时写(delayed write )。减少了读写次数,降低了文件更新速度
void sync(void)
将所有修改过的块排入写队列,然后就返回,并不等待实际写磁盘结束
int fsync(int fd)
只对指定的文件描述符起作用,并且等待实际写入完成才返回,并且同步修改包含文件属性的修改
ret: 成功返回0 , 失败返回 -1
int fdatasync(int fd)
和fsync类似,不过,只作用于数据部分,文件属性不会同步修改
修改文件属性
int fcntl (int fd, int cmd, .../*int arg*/)
ret:成功依赖于cmd, 失败返回 -1
arg:大部分cmd对应的第三个参数是一个整数,但是cmd表示记录锁时,是一个执行结构的指针
cmd 可以分为5类:
(1)复制一个现有描述符 cmd = F_DUPFD
(2)获得/ 设置文件描述符标记 cmd=F_GETFD F_SETFD
(3 )获得/设置文件状态标识 cmd=F_GETFL F_SETFL
(4 )获得/设置异步IO所有权 cmd=F_GETOWN F_SETOWN
(5 )获得/设置记录锁 cmd=F_GETLK F_SETLK F_SETLKW
F_DUPFD 能复制文件描述符,结果为 所有未使用文件描述符中min(未使用描述符最小值,点三个参数)
新的文件描述符有自己的一套文件描述符标识,其中FD_CLOEXEC文件描述符标志被清除(该标志表示在exec 时,关闭文件描述符)
F_GETFD F_SETFD, 当前只定义了FD_CLOEXEC,0 为默认状态,exec 时不关闭描述符
F_GETFL F_SETFL, 设置获取文件标志
文件标志包括,
不可设置:O_RDONLY, O_WRONLY, O_RDWR (0 1 2 ),由于对应012 ,并不是每位对应一种标志,获取后还需要判断
可设置的:O_APPEND, O_NONBLOCK, O_SYNC, O_DSYNC, O_RSYNC,O_ASYNC,O_CLOEXEC,O_NOATIME,O_NOCTTY
O_SYNC 等待写完成(数据和属性) 等于fsync()
通常数据库系统,需要设置设置等待写完成,为了保险起见,最好手动调用fsync()
O_DSYNC 等待写完成 (仅数据) 等于fdatasync()
O_RSYNC 同步读写
O_ASYNC 设置此标记后,当调用open 返回的文件描述符可以进行IO操作时,产生一个信号,仅对终端,FIFOs,socket 生效,Linux中,open 时设置该标记无效,必须通过fcntl 设置才生效
O_CLOEXEC,表示close -on-exec ,一个好的习惯是,在创建(新)文件描述符时,对其设置该标志,可以避免,使用fcntl 设置该标记时,某些多线程下的产生的竞争场景 fork ()和fcntl ()
O_NOATIME,在读文件时,不更新最近访问时间。目的在于设置该标志能显著减少磁盘活动量,省却了既要读取文件内容,又要更新i-node节点的繁琐。使用该标志需要对文件有所有权或特权
O_NOCTTY, 仅当文件是终端文件时有效,防止其成为控制终端。
open 错误号(errno):
EACCES, 权限不允许
EISDIR, 设置了写标志,但是文件是目录
EMFILE, 打开文件描述符到达上限
ENFILE, 文件打开数量达到系统上限(和上面一个属于不同的表)
ENOENT, 文件不存在(文件不存在,目录不存在,链接文件目标不存在)
EROFS, 文件为只读,调用者以写的方式打开文件
ETXTBSY, 文件为可执行文件,且正在执行
int ioctl (int fd, int rquest, ...);
ret:失败返回-1 成功返回其他值
ioctl 除了函数本身的头文件外,还需要设备专用的头文件,以终端设备为例,需要<termios.h>
/dev/ fd
该目录下存在形如/dev/fd/1 /dev/fd/0 的文件,对这些文件进行打卡操作,等效于复制描述符n,
新描述符拥有的权利是已打开文件的子集(打开文件的mode 交集 原有描述符权利), open ("/dev/fd/0" , O_RDWR), 得到的文件描述符依然只能RW, 不能写。
其主要用途用于shell中,使得命令能像处理路径一样处理 标准输入 输出。
某些命令支持特殊符号路径 - ,表示标准输入, 当存在/dev/fd之后,可以用/dev/fd/0 代替 -;
补充说明:
read ,write 都在内核执行
dup本质上是创建一条文件描述符表项,而open 不但会创建文件描述符表项,还会创建打卡文件表项
命令行中 digit1 > &digit2 本质上是调用dup2(digit2, F_DUPFD, digit1) 实现的
当同时存在 重定向输入 重定向输出时,顺序不同可能含义不同,通过上面两条推导
文件IO深入探究
1. 原子性操作:
检测并创建文件,open时同时设置O_CREAT和O_EXCL,当文件存在时,会调用失败
向文件尾追加,使用O_APPEND保证写操作从文件末尾开始
2. 在文件特定偏移处IO
<unistd.h>
ssize_t pread (fd, buf, size, offset )
ssize_t pwrite (fd, buf, size, offset )
与read和write的区别在于,这两个调用会在指定的offset处进行读写,而不是文件偏移处,并且不会改变文件偏移。
相当于原子性的执行了,暂存当前文件偏移,将偏移设置成offset,读写操作,恢复文件偏移。
主要用于多线程场景
3.分散输入和集中输出
<sys/uio.h>
ssize_t readv (fd, struct iovec *iov, int iovcnt )
ret:读取的字节总数,失败返回-1
arg:iov是一个结构体数组,iovcnt是数组长度
功能:将一次读入的数据读到多个buffer当中,每个buffer由一个struct iovec描述
struct iovec {
void *iov_base;
size_t iov_len;
}
ssize_t writev (fd, struct iovec *iov, int iovcnt )
ret:写入的字节总数,失败返回-1
arg:iov是一个结构体数组,iovcnt是数组长度
功能:将多个buffer的内容按照iov的顺序一起写入
等价于,申请一片很大的空间,将iov的buffer拷贝进去,再调用write写
这两个调用主要是为了使用方便
linux 2.6.30版本之后还支持
<sys/uio.h>
preadv (fd, *iov, iovcnt, offset )
pwritev (fd, *iov, iovcnt, offset )
文件和目录
stat 函数和结构体
#include <sys/stat.h>
int stat (filename, struct stat *buf)
int fstat (fd, struct stat *buf)
int lstat (filename, struct stat *buf)
ret: 成功返回0 ,失败返回-1
arg: buf是一个出参,用于保存filename 的stat
stat和fstat的区别在用使用路径还是描述符指定文件,
lstat和stat类似,但是当文件是符号链接时,lstat返回符号链接本身的信息,而不是链接指向的目标文件
struct stat
{
dev_t st_dev;
ino_t st_ino;
mode_t st_mode;
nlink_t st_nlink;
uid_t st_uid;
gid_t st_gid;
dev_t st_rdev;
off_t st_size;
blksize_t st_blksize;
blkcnt_t st_blocks;
time_t st_atime;
time_t st_mtime;
time_t st_ctime;
};
文件类型及判断方法
unix文件类型:
普通文件(regular file)
目录文件
块特殊文件
字符特殊文件
FIFO(命名管道)
套接字(socket)
符号链接
<sys/stat.h> 中定义了一些判断文件类型的宏
S_ISREG(mode )
S_ISDIR(mode )
S_ISCHR(mode )
S_ISBLK(mode )
S_ISFIFO(mode )
S_ISSOCK(mode ) 在linux系统下,需要定义_GNU_SOURCE才能使用该接口
IPC对象被视为文件,但是其接口有点区别,不是mode 而是指向stat的指针
S_TYPEISMQ(ptr) 是否为消息队列
S_TYPEISSEM(ptr) 是否为信号量
S_TYPEISSHM(ptr) 是否为共享内存
stat-文件访问权限
实际用户ID,有效用户ID,保存的设置用户ID,
实际组ID,有效组ID,附加组ID,保存的设置组ID
实际用户ID,即登录时输入的用户名,密码对应的用户
有效用户ID,决定了我们的文件访问权限,
保存的设置用户ID, 在程序执行时,保存的有效用户id
每个文件都有一个所有者,由stat中st_uid成员表示, st_gid表示所有组
通常进程的有效ID等于用户的实际ID,但是可以在通过设置stat中的st_mode设置一个特殊标志,改变这种行为,
设置用户ID(set-user-ID):"当执行文件时,进程的有效用户id设置成文件所有者的用户id" , 对应的宏为S_ISUID
设置组ID(set-group-ID):"将执行此文件的进程的有效组id,设置成文件所有者的组ID" ,对应的S_ISGID
通过passwd命令,和shadow文件来理解这两个标志
文件访问权限:
所有文件都有访问权限,由三组 3b it位表示(rw- r-- r--),三bit分别代表 读 写 可执行,每组代表不同角色,表示所有者,所有组,其他用户
访问权限为:
{
<sys/stat.h>
S_IRUSR
S_IWUSR
S_IXUSR
S_IRGRP
S_IWGRP
S_IXGRP
S_IROTH
S_IWOTH
S_IXOTH
}
目录权限:
当打开文件时,对文件名字中的目录(包含隐含的当前工作目录),校验是否具有可执行权限;然后判断文件本身的是满足访问权限
引用隐含目录的例子:访问不带目录名的文件,会隐式的检查当前目录的执行权限。对于PATH变量,如果某个目录不具备可执行权限,shell不会在该目录下搜索文件
目录的读权限,允许获得目录下的文件列表;
目录的执行权限,使用目录下的文件;
目录的写权限,在目录下创建/删除文件时,需要同时有目录的写和执行权限。 令人奇怪的是,删除文件不需要对文件本身有读写权限
权限性质:
文件所有者(st_uid、st_gid),是文件性质
进程的有效ID (有效用户ID,有效组ID),是进程性质
权限校验过程
若进程的有效ID是0 (超级用户),允许访问。
若进程有效ID等于用户所有者ID,会按照打开文件的mode & 文件用户权限 获得访问权限
若进程有效组ID或附加组ID之一等于文件组ID,则会按照文件的打开mode & 文件组权限获得访问权限
若其他用户权限被设置,则可访问
新文件和目录的所有权
新文件的用户ID为进程的有效ID。
新文件的组ID可以是进程的有效组ID,或者是他所在目录的组ID,取决于操作系统(Linux中,如果父目录存在组ID,则新文件组ID于父目录相同)
#include <unistd.h>
int access (path, mode)
ret:成功返回0 ,失败返回-1
arg:mode常用宏 R_OK, W_OK,X_OK, F_OK
access和进程访问文件略有不同,是按照 实际用户/组ID 进行访问权限测试的,单是校验过程一样
所以,当设置了 设置用户ID 之后,可能出现access R_OK失败,但是文件能以可读方式打开
#include <sys/stat.h>
mode_t umask(mode_t cmask);
ret: 之前设置的mask值
创建文件是,mask为1 的为,会屏蔽掉open,creat 中mode对应的位
创建文件前,先清0 umask值,避免创建文件影响其他进程读取,
每个进程都有一个独自的umask值(非全局唯一)
命令行中 umask -S 可以查看未屏蔽
#include <sys/stat.h>
int chmod (const char *path, mode_t mode) ;
int fchmod (int fd, mode_t mode) ;
ret: 成功返回0 ,失败返回-1 ;
要求,进程的有效用户ID 等于 文件的所有者ID,或者root用户
chmod 可以设置除了上述常规权限外,还有三个特殊权限,
S_ISUID, 设置用户id
S_ISGID, 设置组id
S_ISVTX, 粘贴位/正文为,saved-text bit
当权限不够时,设置的特殊权限会被清除,
ls -l 时,
-rwsr-xr-x 表示SUID和所有者权限中可执行位被设置
-rwSr--r-- 表示SUID被设置,但所有者权限中可执行位没有被设置
-rwxr-sr-x 表示SGID和同组用户权限中可执行位被设置
-rw-r-Sr-- 表示SGID被设置,但同组用户权限中可执行位没有被设置
chmod 实际上是更新的是inode节点最近被修改的时间,而ls -l 显示的时间是文件内容最后被修改的时间
粘贴位,S_ISVTX
通常对目录设置粘贴位,
对目录具有写权限,并且【拥有此文件||拥有此目录||超级用户】才能 删除或者更改 目录下的文件名
典型应用场景是:/tmp 和 /var/spool/uucppublic
任何人可以在目录下创建文件,并读写,但是只能修改自己拥有的文件
三个特殊权限对应的二进制位,在原有权限位前加三个,4 2 1 对应SUID SGID sticky,和其他权限组合起来的八进制数7 777
#include <unistd.h>
int chown (path, uid_t owner, gid_t group)
int fchown (fd, uid_t owner, gid_t group)
int lchown (path, uid_t owner, git_t group)
ret: 成功返回0 ,失败返回-1
owner 或 group为-1 时,表示不改变
_POSIX_CHOWN_RESTRICTED, 特殊的权限设置符号,如果该符号起作用,只有root用户能更改其用户ID,除非文件原owner为-1 且对文件有写权限
文件长度
st_size
仅普通文件,目录,符号链接有意义,分别对应文件长度,目录项记录长度,链接中实际的字节数(不含null,usr/lib长为7 )
使用st_blksize 或 st_blocks 表示文件占用的IO块数,使用du -s [file] 可以查看磁盘块的占用数
空洞文件:
将指针偏移至文件末尾之后,进行行写操作之后得到的。
如果使用cat 读取空洞文件,会将空洞内容以0 填满,导致变为空洞文件长度变为实际文件长度
#include <unistd.h>
int truncate (path, off_t length)
int ftruncate (fd, off_t length)
ret: 成功返回0 , 出错返回-1
将文件截断为指定长度,如果文件原长度小于length,效果因系统而异
文件系统
磁盘(分区)
分区(文件系统)
文件系统(自举块,超级块,柱面0 ,柱面1. ..柱面n)
柱面i(超级块副本,配置信息,i节点图,块位图,i节点,数据块)
i节点(i节点1 ,i节点2 ,i节点3. ..)数据块(空白,数据块1 ,空白,数据块2. 空白,目录块1 ,空白...)
1. i节点是固定长度的记录项,每个i节点n中有指向数据块的指针,目录录块也是一段数据,不过内容是(指向i节点n的索引和文件名)
2. 一个i节点n可以指向多个数据块,多个目录中的目录项也可以指向同一个i节点(会使节点的链接计数+1 ),只有当链接数为0 时,才会释放占用的磁盘快,这也是删除目录项被称为unlink的原因。
3. 链接计数包含在 st_nlink 中。基本类型为 nlink_t 称为硬链接, LINK_MAX指定了一个文件链接数的最大值
4. 对于符号链接: inode 中会指明文件类型是S_IFLNK, 符号链接的内容,是实际文件的路径名
5. i节点包含的信息:文件类型,文件访问权限位,文件长度,指向该文件所占用的数据块的指针
6. stat中大部分信息都取自i节点,只有两项数据取自目录项,i节点编号ino_t 和 文件名
7. 每个文件系统都对各自的i节点进行编号,这就是为什么ln命令不能跨文件系统创建
8. 在不更换文件系统的情况下为文件更换文件名时,文件的实际内容没有改动,只需要构造一个的目录项,并解除与旧目录项的链接,通常mv的工作方式就是如此
9. 每个不包含其他目录的目录,链接计数都是2 因为存在'.' 目录,要是目录包含目录项,链接计数至少为3 ,因为除了本身和'.' 外,还有子目录的'..'
#include <unistd.h>
int link(exitpath, newpath)
ret: 成功返回0 ,失败-1
arg:exitpath指向现有文件,newpath链接文件
为现有文件创建一个链接
#include <unistd.h>
int unlink(path)
ret:成功返回0 ,失败返回-1 ;
删除一个目录项,使文件计数减一,
当path是符号链接时,unlink会删除符号链接,而不是引用文件本身
pathname 也可以是目录,但是通常使用rmdir函数
#include <stdio.h>
int remove(path)
ret: 成功返回0 , 失败 -1
对于remove,如果path是文件,相当于unlink,如果path是目录,相当于
#include <stdio.h>
int rename(oldname, newname)
ret:成功返回0 ,失败返回-1
如果old是文件,如果new已经存在,则删除源文件
如果old是目录,如果new已经存在,
如果old 是符号链接,直接修改其本身,而不是引用
符号链接
硬链接的限制:两个链接处于同一文件系统,只有超级用户才能创建指向目录的硬链接(应为操作不当可能在系统中出现循环,而大多数系统无法处理这种情况)
符号链接一般用于将整个文件或目录移动到系统的另一位置
#include <unistd.h>
symlink(path, sympath)
ret:成功返回0 , 失败返回 -1
arg:path为目标,sympath为符号链接
创建符号链接,要注意path可以不存在
#include <unistd.h>
ssize_t readlink(path, buf, buf_size)
ret: 成功返回读到的字节数,失败返回-1
返回符号链接本身的内容,同样,不易null结尾
文件的时间
三个时间:
最后访问时间:st_atime, ls -u
最后修改时间:st_mtime, ls
i节点最后修改时间:st_ctime, ls -c
文件的访问和修改时间可以被更改:
int utime(pathname, struct utimbuf *times )
ret:成功返回0,失败返回-1
struct utimbuf {
time_t actime;
time_t modtime;
}
如果times 是空指针,则设置为当前时间
如果是非空指针,只有当进程有效id 等于文件所有者用户id 、或者超级用户进程时,才能修改成功
目录
创建目录
#include <sys/stat.h>
int mkdir(path, mode)
ret: 成功返回0 ,失败返回-1
至少要对目录设置一个执行权限
删除目录
#include <unistd.h>
int rmdir(path)
ret: 成功返回0 ,失败返回-1
调用此函数时,目录的连接数0 ,如果有进程正在访问该目录,也不会失败,等到进程退出时才真正删除目录。
但是调用后,其他进程不能再访问该目录
读取目录
#include <dirent.h>
DIR *opendir(path)
struct dirent *readdir(DIR *dp)
void rewinddir(DIR *dp)
int closedir(DIR *dp)
void seekdir(DIR *dp, long loc)
struct dirent {
ino_t d_ino; \\i节点号
char d_name[NAME_MAX +1 ]; \\max通常为255 ,文件名以null结尾
}
例题:递归遍历目录文件
改变工作目录
#include <unistd.h>
int chdir(path)
int fchdir(fd)
ret: 成功返回 0 , 失败返回-1
当前工作目录是一个进程的一个属性,登录用户目录是登录名的一个属性
获取当前工作目录
#include <unistd.h>
char *getcwd(buf, size)
ret:成功返回buf,失败返回NULL
关于fchdir,平常我们可以,使用open打开目录,获得一个描述符,可以使用fchdir
特殊文件
设备号的数据类型是dev_t
主设备号和设备驱动程序相关,
次设备号标识特定的子设备。
通常有两个宏能够访问major(),minor()。定义在头文件 <sys/sysmacros.h> 中,而该文件被<sys/type.h>包含
每个文件都有st_dev值,表示文件系统的设备号,只有特殊文件和块特殊文件才有st_rdev值
shell 正则 tty [01] 表示 tty0 tty1
通常文件都是磁盘的主从设备号,st_dev, 设备文件使用的是设备自己的设备号st_rdev
使用mount命令可以查看 主从设备号
习题
core文件产生时 权限有可能不受mask 值的影响 和系统有关
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了