文件描述符
按惯例,UNIX
系统shell
把:
文件描述符0
与进程的标准输入关联。
文件描述符1
与标准输出关联。
文件描述符2
与标准错误关联。
对应的宏定义为:
STDIN_FILENO
STDOUT_FILENO
STDERR_FILENO
定义于<unistd.h>
.
open
#include <fcntl.h>
// 出错返回-1,成功返回文件描述符
// path 打开或者创建文件名字,
// oflag
// 5选1标志
// O_RDONLY
// O_WRONLY
// O_RDWR
// O_EXEC
// --O_SEARCH[应用于目录]
// 可选
// O_APPEND
// O_CLOEXEC
// O_CREAT 需要同时指定mode[权限位集合]
// O_EXCL O_CREAT|O_EXCL未存在,创建;
// 已存在,返回错误;测试&创建为原子操作
// O_TRUNC 如文件存在,要求写权限时,打开时清空文件
int open(const char* path, int oflag, ...);
lseek
每个打开文件有一个与其关联的“当前文件偏移量”。通常,读/写操作都从当前文件偏移量处开始,并使偏移量增加所读/写的字节数。
#include <unistd.h>
// 设置fd对应文件的偏移量
// 如果文件描述符,指向的是一个管道,FIFO,网络套接字,则lseek返回-1,将errno设为ESPIPE
// 若whence是SEEK_SET,设置偏移量为距文件开始处offset个字节
// 若whence是SEEK_CUR,设置偏移量为当前值+offset, offset有符号
// 若whence是SEEK_END,设置偏移量为文件长度+offset,offset有符号
off_t lseek(int fd, off_t offset, int whence);
通常,当前文件偏移量应当为非负整数。但,某些设备也允许负的偏移量。但对于,普通文件,其偏移量需要是非负值。lseek
仅将当前文件偏移量记录在内核,不引起I/O
操作。该偏移量用于下一个读/写操作。
文件偏移量可以大于文件当前长度,这时,对文件的下一次写将加长文件,并在文件中构成一个空洞,对文件中没写过的字节都被读为0
。文件中的空洞并不要求在磁盘占用存储区。
read
//#include <unistd.h>
// 返回已经读到的字节数,读时已到文件尾时,返回0。若出错,返回-1。
// 实际读到的字节数少于要求读的字节数
// - 读普通文件时,在读到要求字节数之前已经达到了文件尾端
// - 从终端设备读时,通常一次最多读一行
// - 从网络读时,网络的缓冲机制可能造成返回值小于所要求读的字节数
// - 从管道或FIFO读时,若管道包含的字节少于所需的数量,read只返回实际可用的字节数
// - 当从某些面向记录的设备(如磁带)读时,一次最多返回一个记录
// - 当一信号造成中断,而已经读了部分数据量时。
// 读操作从文件当前偏移量开始,成功返回前,该偏移量将增加实际读到的字节数。
ssize_t read(int fd, void* buf, size_t nbytes);
write
//#include <unistd.h>
// 成功,返回已经写的字节数。若出错,返回-1。
// 返回值通常与nbytes相同,否则,表示出错。
// 常见出错原因,磁盘已经写满,或超过了一个给定进程的文件长度限制
// 对普通文件,写操作从文件当前偏移量处开始。
// 指定O_APPEND下,每次写之前,先将文件偏移量设置到文件当前结尾
// 一次写成功后,文件偏移量增加实际写的字节数。
ssize_t write(int fd, const void* buf, size_t nbytes);
I/O效率
read/write
一次按文件系统磁盘块大小进行读写时具备较高效率.
文件共享
概念上,内核使用3
中数据结构表示打开文件。
(1). 每个进程在进程表中有一个记录项,记录项中包含一张打开文件描述符表。每个描述符占一项。
与每个文件描述符关联的是:
文件描述符标志
指向一个文件表项的指针
(2). 内核为所有打开文件维持一张文件表。
每个文件表项包含:
文件状态标志【读/写/添写/同步/非阻塞等】
当前文件偏移量
指向文件v
节点表项的指针
(3). 每个打开文件或设备都有一个v
节点结构。
v
节点包含了文件类型和对此文件进行各种操作函数的指针。
对大多数文件,v
节点还包含该文件的i
节点(索引节点)。
i
节点包含了文件的所有者,文件长度,指向文件实际数据块在磁盘上所在位置的指针等.
两个独立进程各自打开同一文件,进程有独立的文件描述符表,打开的同一文件对应两个描述符表中独立的两项
。独立的指向两个文件
表项。但是两个文件表项v
节点指针指向此文件的同一个v
节点结构。
- 在完成每个
write
后,文件表项中当前文件偏移量进行更新,如导致偏移量超出了当前文件长度,则将i
节点表现的当前文件长度设置为当前文件偏移量。 - 如果用
O_APPEND
标志打开一个文件,则相应标志被设置到文件表项的文件状态标志中。每次对这种具有追加写标志的文件执行写操作时,文件表项中的当前文件偏移量首先会被设置为i
节点表项中的文件长度。使得每次写入的数据都追加到文件的当前尾端处。 - 若一个文件用
lseek
定位到文件当前的尾端,则文件表项中的当前偏移量被设置为i
节点表项中的当前文件长度。 lseek
只修改文件表项中的当前文件偏移量,不进行I/O
操作。
可能有多个文件描述符指向同一文件表项。文件描述符标志只用于一个进程的一个描述符。文件状态标志则应用于指向该文件表项的任何进程的所有描述符。dup/fork
下基于原始描述符产生了一个新描述符和原始描述符指向相同的文件表项.
dup
int dup(int fd);
上述API
执行结果时,进程中包含两个指向同一个文件表项的文件描述符。新的文件描述符的文件描述符标志FD_CLOEXEC
是初始清除掉的。
sync,fsync
传统UNIX
内核设有缓冲区高速缓存,页高速缓存,多数磁盘I/O
通过缓冲区进行。即read/write
并非直接从磁盘读,写入磁盘.在read/write
和磁盘间还有一层内核缓冲区.以便高效的与磁盘交互.向文件写数据,内核常先将数据复制到缓冲区,排入队列,晚些再写入磁盘。称为延迟写。内核需要重用缓冲区来存放其他磁盘块数据时,会把延迟写数据块写入磁盘。
// 对fd指定的一个文件,保证对此文件的修改全部写入磁盘后&文件结点信息更新后才返回
int fsync(int fd);
// 类似fsync,但只影响文件的数据部分。fsync会保证文件数据和文件属性的同步更新。
//int fdatasync(int fd);
// 将所有修改过的块缓冲区排入写队列,立即返回
void sync();
fcntl
int fcntl(int fd, int cmd, ...);
F_DUPFD 复制文件描述符fd,返回新的描述符。
它是尚未打开的描述符中大于等于参数3的各值中最小值。
新描述符与fd共享同一文件表项。
其FD_CLOEXEC文件描述符标志被清除。
F_DUPFD_CLOEXEC F_DUPFD+设置新描述符的FD_CLOEXEC文件描述符标志。
F_GETFD 返回fd对应的文件描述符标志。
F_SETFD 对fd按参数3设置其文件描述符标志。
F_GETFL 返回fd对应文件表项的文件状态标志。
文件状态标记&O_ACCMODE得到5个互斥模式中一个。
F_SETFL 将文件状态标志设置为参数3。
可更改的几个标志为:
O_APPEND,
O_NONBLOCK,
// O_SYNC,
// O_DSYNC,
// O_RSYNC,
O_FSYNC,
// O_ASYNC。
F_GETOWN 获取当前接收SIGIO,SIGURG信号的进程ID/进程组ID。
F_SETOWN 设置接收SIGIO/SIGURG信号的进程ID或进程组ID。
正的参数3代表进程ID。
负的参数3代表进程组ID为abs(参数3)。
stat,lstat
int stat(const char* restrict pathname, struct stat* restrict buf);
int lstat(const char* restrict pathname, struct stat* restrict buf);
struct stat
{
mode_t st_mode;
ino_t st_ino;
dev_t st_dev;
dev_t st_rdev;
nlink_t st_nlink;
uid_t st_uid;
gid_t st_gid;
off_t st_size;
struct timespec st_atime;
struct timespec st_mtime;
struct timespec st_ctime;
blksize_t st_blksize;
blkcnt_t st_blocks;
};
两者的差别在于,文件指向一个符号链接时,stat
返回符号链接所链接文件信息,lstat
返回文件自身信息.
文件类型
(1). 普通文件
包含了某种形式的数据。至于数据是文本还是二进制,对UNIX
内核而言无区别。
(2). 目录文件
包含了其他文件的名字,及指向与这些文件有关信息的指针。只有内核可以直接写目录文件。
(3). 块特殊文件/字符特殊文件
区别在于,一个提供对设备(如磁盘)带缓冲的访问,每次访问以固定长度为单位进行。一个提供对设备不带缓冲的访问,每次访问长度可变。系统中的所有设备要么是字符特殊文件,要么是块特殊文件。
(4). FIFO
/管道/套接字/消息队列/信号量/共享存储对象
本机进程间通信,不同主机进程间通信.
(5). 符号链接
指向另一个文件.
// 确定文件类型
#include <sys/stat.h>
stat st;
...
S_ISREG(st.st_mode) 普通文件
S_ISDIR(st.st_mode) 目录
S_ISCHR(st.st_mode) 字符特殊文件
S_ISBLK(st.st_mode) 块特殊文件
S_ISFIFO(st.st_mode) 管道或FIFO
S_ISSOCK(st.st_mode) 套接字
S_ISLINK(st.st_mode) 符号链接
S_ISUID(st.st_mode) 测试设置用户ID
S_ISGID(st.st_mode) 测试设置组ID
对文件可读/可写/可执行理解
对于普通文件而言:
(1). 读权限允许用户读取该文件;
(2). 写权限允许用户修改该文件;
(3) 执行权限允许用户执行该文件:
a. 对于一个不可执行的文件来说,拥有执行权限是没有任何意义的;
b. 如果文件是一个程序或者某种类型的脚本时,那么它就需要执行权限来运行.
对于目录而言:
(1). 读权限允许用户读取目录中的文件名,只能列举目录中的文件名,不能进入该目录,相应也不能查看目录下各文件的大小;
(2). 写权限允许用户标修改目录(创建、移动、复制、删除);
(3). 执行权限允许用户搜索该目录;
设置用户ID/设置组ID
对进程:
(1). 实际用户ID
/实际组ID
取决于当前登录系统的用户.
(2). 有效用户ID
/有效组ID
是进程访问文件时,执行访问权限检查的依据.
默认下,采用实际用户ID
/实际组ID
的值.如以管理员权限运行程序时,其可能就与实际的不一致了.
(3). 保存的设置用户ID
和保持的设置组ID
用于记录有效用户ID
/有效组ID
值.
对文件:
所有者ID
组所有者ID
可通过stat.st_uid/stat.st_gid
获得
文件的stat.st_mode
还包含设置用户ID
位和设置组ID
位.意思是执行此文件时,依据此文件的所有者ID
/组所有者ID
来设置进程的有效用户ID
/组ID
设置.(如文件模式中无上述标志位,则执行进程的有效用户ID
/组ID
依据进程的实际用户ID
/组ID
来设置的).
典型应用举例:
一个可执行文件A
,文件的用户和组是root/root
.
A
在执行中需要写一个文件B
,文件B
的用户和组是root/root
.
文件B的权限组合为:
对文件用户本身:可读,可写
对文件组所在的用户:可读
对其他用户:可读
现在我们以普通用户X
登录系统,如果我们直接执行A
.则执行进程的实际用户和组为X/X
(假设组和用户一致).执行进程的有效用户和组也为X/X
.在执行到对文件B
的写访问处,我们是在以其他用户的身份对B写,权限检查不通过,会导致访问拒绝.
此时,我们要想正确完成A
的执行.要么,以root
用户登录系统,再运行A
.
要么,对文件A
将其设置用户ID
标志设置为1
.此时,执行进程的实际用户/组为X/X
,有效用户/组为root/root
(依据文件A的用户/组来设置的).此时执行到对文件B
的写访问处,我们是在以B
的用户对B
写,权限检查通过.
文件访问权限
打开某一文件时,对层次上经过的所有目录都要具备对相应目录文件的执行权限(以便在上级目录中通过一个子目录名/文件名对目录进行查找).
注意:
文件权限检查,依据的是进程的有效用户ID
/有效组ID
/附属组ID
和文件的所有者ID
/所有者组ID
.
三个等级匹配次序:用户匹配,组匹配,其他匹配.先确定匹配,后在匹配内进行权限检查.匹配内权限检查不通过,直接失败.不会接着按其他等级匹配作检查.
access
依据进程的实际ID
/实际组ID
执行对文件执行权限测试:
int access(const char* pathname, int mode);
umask
// 进程用open创建文件时,文件的模式为 参数mode & ~(cmask)
mode_t umask(mode_t cmask);
chmod
进程更改现有文件的访问权限.进程为超级进程或进程的有效ID
等于文件的所有者ID
,进程才有权限对文件执行此函数.
int chmod(const char* pathname, mode_t mode);
chown
更改已经存在文件的ID
/组ID
.
int chown(const char* pathname, uid_t owner, gid_t group);
int lchown(const char* pathname, uid_t owner, gid_t group);
文件长度
stat
的st_size
表示以字节为单位的文件长度。只对普通文件,目录文件,符号链接有意义。
文件截断
修改一个已经存在文件可访问长度.
int truncate(const char* pathname, off_t length);
文件系统
(1). 每个i
节点有一个链接计数(指向该i
节点的目录项数)
链接计数为0
时,可释放此i
节点所指向的数据块集合.
i
节点的链接计数,通过stat.st_nlink
可在代码中获取,图中的链接为硬链接.
(2). 一种叫做符号链接的文件,在该文件i
节点指向数据块中存储了一个文件路径(为其所链接的文件).
对应文件类型常量为S_IFLNK
.
(3). i
节点含有
文件类型/访问权限.长度/数据块指针集合等信息.
(4). 目录块每一项
i
节点编号(必须为同一文件系统下的),文件名.
(1). 1267
代表了一个包含testdir
项的目录文件.testdir
是一个目录文件.
一个目录文件链接计数至少为2
.一个链接计数来自目录自身数据块中,文件名为.
的项的i
节点编号为目录文件的i
节点编号.一个链接计数来自所在目录数据块中,文件名为此目录名的项的i
节点编号为目录文件的i
节点编号.
link,unlink
// 新增一个目录项,目录项的i节点为existingpath的i节点
// 更新i节点的引用计数
int link(const char* existingpath, const char* newpath);
// 删除一个目录项,更新i节点中引用计数
int unlink(const char* pathname);
即使一个i
节点的引用计数变为0
,但如果此i
节点此时被文件表项所指向(也即处于打开状态),此i
节点也不会被删除.当此i节点无文件表现指向,引用计数又是0
时,系统会释放此i
节点,i
节点占据的磁盘相关空间会被回收.
rename
int rename(const char* oldname, const char* newname);
符号链接
(1). 用link
创建的是硬连接,底层文件仍然只有一个i
节点.
(2). 符号链接的产生一个独立的文件,此文件的内容是其所链接文件的路径名.
(3). 一般的文件I/O
函数,传入符号链接文件时,会自动替换为其所链接文件的路径再进行后续处理.
创建和读取符号链接
int symlink(const char* actualpath, const char* sympath);
// 读出符号链接文件自身内容
ssize_t readlink(const char* restrict pathname, char *restrict buf, size_t bufsize);
文件的时间
utimes
人为设置文件的最后访问/修改时间.
int utimes(const char* pathname, const struct timeval times[2]);
struct timeval
{
time_t tv_sec;
long tv_usec;
};
mkdir, rmdir
int mkdir(const char* pathname, mode_t mode);
// 待操作的目录需为空的(只含两项.及..)
// 若目录文件引用计数因此函数而变为0,则i节点及指向的磁盘块后续被释放
int rmdir(const char* pathname);
读目录
目录文件结构依赖特定实现,需借助系统API
由系统与其交互.
DIR* opendir(const char* pathname);
struct dirent* readdir(DIR* dp);
int closedir(DIR* dp);
chdir, getcwd
设置和获取进程当前目录
int chdir(const char* pathname);
char* getcwd(char* buf, size_t size);