文件描述符
FILE结构体
查看stdio.h
头文件中,有FILE
结构体的定义:
//stdio.h
typedef struct _iobuf {
char* _ptr; //文件输入的下一个位置
int _cnt; //当前缓冲区的相对位置
char* _base; //文件初始位置
int _flag; //文件标志
int _file; //文件有效性
int _charbuf; //缓冲区是否可读取
int _bufsiz; //缓冲区字节数
char* _tmpfname; //临时文件名
} FILE;
其中_file
就是文件描述符。
文件描述符
文件描述符(fd
,file descriptor
)是文件IO(也系统IO)中贯穿始终的类型。如下图所示:
- 内核会维护一张存有各进程的列表,谓之进程表。系统中每个进程在进程表中都占用一个表项,每个表项都包含了对应的描述信息,入进程ID、用户ID、组ID等,其中也包含了一个被称为文件描述符的数据结构
- 当某一个进程执行系统调用
open
函数,会创建一个结构体,该结构体类似于FILE结构体,其中最基本的成员有一个指针pos
,用于指向inode
文件的某一个位置; - 同时,该进程会维护一个数组(文件描述符表),该数组存储上述结构体的地址,而数组下标就是文件描述符
fd
,即文件描述符的本质就是一个整型数;- 该数组默认大小为1024,即可以打开的最大文件数量为1024,但可以设置
ulimit
来更改数组大小;注意该数组和对应产生的结构体只存在于这个进程空间内,而不是进程间共享; - 当调用
open
函数时,系统会自动打开三个流stdin
,stdout
和stderr
,这三个流分别占据该数组的0,1,2
号位置; - 结构体FILE中的成员
_file
就是整型数组下标fd
,即文件描述符 - 每打开一个新文件,则占用一个数组空间,而且是空闲的最小的数组下标。即文件描述符优先使用当前可用范围内最小的。同一个文件可以被多次打开,但是每打开一次都需要一个新的文件描述符和新的结构体,例如图中的结构体1和结构体2,指向了同一个inode;
- 该数组默认大小为1024,即可以打开的最大文件数量为1024,但可以设置
- 执行系统调用
close
时,就将对应fd下标的数组空间清除掉,并清除该地址指向的结构体; - 结构体中有一个成员用于记录引用计数,例如图中,将5号位置的
0x006
地址复制一份存储在6号位置,此时有两个指针指向了同一个结构体3,此时结构体3的引用计数为2,当5号指针free
时,结构体3的引用计数减1为1,不为0,则不会释放掉,否则6号位置的指针将成为野指针; - 每关闭一个文件描述符,无论被其索引的文件表项和v节点是否被删除,与之对应的文件描述符表项一定会被标记为未使用,并在后续操作中为新的文件描述符所占用
- 系统内核默认为每个进程打开三个文件描述符,他们在unistd.h头文件中被定义为宏
#define STDIN_FILENO 0 //标准输入
#define STDOUT_FILENO 1 //标准输出
#define STDERR_FILENO 2 //标准错误
- 这个文件描述符是该进程自己的文件描述符表中的索引。因此,对于每个进程来说,文件描述符的值是相对于该进程的文件描述符表而言的。
- 与内核文件表不同,内核文件表是内核维护的一个数据结构,用于保存每个打开文件的元数据信息。但是,文件描述符与内核文件表是相互关联的,因为内核通过文件描述符来识别和跟踪已打开的文件。
- 需要注意的是,不同的进程可能有相同的文件描述符值,但这并不意味着它们指向的是同一个文件。这是因为每个进程都有自己的文件描述符表和文件指针,它们是相互独立的。
文件的内核结构
-
一个处于打开状态的文件,系统会为其在内核中维护一套专门的数据结构,保存该文件的信息,直到它被关闭
- v节点与v节点表
- 文件的元数据和在磁盘上的存储位置都保存在其i节点中,而i节点保存在分区柱面组的i节点表中,在打开文件时将其i节点信息读入内存,并辅以其他的必要信息形成一个专门的数据结构,势必会提高对该文件的访问效率,这个存在于进程的内核空间,包含文件i结点信息的数据结构被称为v节点。多个v节点结构以链表的形式构成v节点表
- 文件表项与文件表
- 由文件状态标志(来自open函数的flags参数)、文件读写位置(最后依次读写的最后一个字节的下一个位置)和v节点指针等信息组成的内核数据结构被称为文件表项。通过文件表项一方面可以实时记录每次读写操作的准确位置,另一方面可以通过v节点指针访问包括该文件各种元数据和磁盘位置在内的i节点信息。多个文件表项以链表形式构成文件表
- v节点与v节点表
-
多次打开同一个文件,无论是在同一个进程中还是在不同的进程中,都只会在系统内核中产生一个v节点
-
每次打开文件都会产生一个新的文件表项,各自维护各自的文件状态标志和当前文件偏移,却可能因为打开的是同一个文件而共享同一个v节点
-
打开一个文件意味着内存资源(v节点、文件表项等)的分配,而关闭一个文件其实就是为了释放这些资源,但如果所关闭的文件在其他进程中正处于打开状态,那么v节点并不会被释放,直到系统中所有曾打开过该文件的进程都显示或隐式地将其关闭,其v节点才会真正被释放
-
一个处于打开状态的文件也可以被删除,但它所占有的磁盘空间直到它的v节点彻底小时以后才会被标记为自由
文件打开与关闭
open
open
用于打开或创建一个文件或者设备。
函数原型1
int open(const char *pathname, int flags);
- 将准备打开的文件或是设备的名字作为参数path传给函数,flags用来指定文件访问模式。
- open系统调用成功返回一个新的文件描述符,失败返回
-1
。
其中,flags是由必需文件访问模式和可选模式一起构成的(通过按位或|
):
必需部分 | 可选部分(只列出常用的) |
---|---|
O_RDONLY :以只读方式打开 |
O_CREAT :按照参数mode 给出的访问模式创建文件 |
O_WRONLY :以只写方式打开 |
O_EXCL :与O_CREAT 一起使用,确保创建出文件,避免两个程序同时创建同一个文件,如文件存在则open调用失败 |
O_RDWR :以读写方式打开 |
O_APPEND :把写入数据追加在文件的末尾 |
O_TRUNC :把文件长度设置为0,丢弃原有内容 |
|
O_NONBLOCK :以非阻塞模式打开文件 |
其中,对于可选部分,又分为文件创建选项和文件状态选项:
- 文件创建选项:
O_CREAT
,O_EXCL
,O_NOCTTY
,O_TRUNC
- 文件状态选项:除文件创建选项之外的选项
fopen和open的文件访问模式的联系
r -> O_RDONLY // 只读存在的文件
r+ -> O_RDWR // 读写存在的文件
w -> O_WRONLY|O_CREAT|O_TRUNC // 只写,并且有则清空,无则创建
w+ -> O_RDWR|O_CREAT|O_TRUNC // 读写,并且有则清空,无则创建
// ...
函数原型2
int open(const *path, int flags, mode_t mode);
在第一种调用方式上,加上了第三个参数mode
,主要是搭配O_CREAT
使用,这个参数规定了属主、同组和其他人对文件的文件操作权限。只列出部分:
字段 | 含义 |
---|---|
S_IRUSR |
读权限 |
S_IWUSR |
写权限 ——文件属主 |
S_IXUSR |
执行权限 |
可以用数字设定法:
数字 | 含义 |
---|---|
0 | 无权限 |
1 | x |
2 | w |
4 | r |
注意mode还要和umask计算才能得出最终的权限;
例如:
int fd = open("./file.txt",O_WRONLY | O_CREAT, 0600);
创建一个普通文件,权限为0600
,拥有者有读写权限,组用户和其他用户无权限。
补充:变参函数
变参数函数的原型声明为:
type VAFunction(type arg1, type arg2, ...);
变参函数可以接受不同类型的参数,也可以接受不同个数的参数。
参数可以分为两部分:个数确定的固定参数和个数可变的可选参数。函数至少需要一个固定参数,固定参数的声明和普通函数一样;可选参数由于个数不确定,声明时用 ...
表示。固定参数和可选参数共同构成一个函数的参数列表。
以printf
为例,它就是一个变参函数:
int printf(const char *fmt, ...){
int i;
int len;
va_list args; /* va_list 即 char * */
va_start(args, fmt);
/* 内部使用了 va_arg() */
len = vsprintf(g_PCOutBuf,fmt,args);
va_end(args);
for (i = 0; i < strlen(g_PCOutBuf); i++)
{
putc(g_PCOutBuf[i]);
}
return len;
}
close
#include<unistd.h>
int close(int fd);
功能:关闭处于打开状态的文件描述符
参数:fd 处于打开状态的文件描述符
//文件的打开和关闭
#include<stdio.h>
#include<fcntl.h>// open();
#include<unistd.h>// close()
int main(void){
//打开文件
int fd=open("./open.txt",O_RDWR|O_CREAT|O_TRUNC,0777);
if(fd == -1){
perror("open");
return -1;
}
printf("fd = %d\n",fd);
//关闭文件
close(fd);
return 0;
}
文件读写
write
#include<unistd.h>
ssize_t write(int fd,void const* buf,size_t count);
-
功能:向指定文件写入数据
-
参数:
- fd 文件描述符
- buf 内存缓冲区,即要写入的数据
- count 期望写入的字节数
-
返回值:成功返回实际写入的字节数,失败返回-1
//向文件中写入数据
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>
int main(void){
//打开文件
int fd = open("./shared.txt",O_WRONLY|O_CREAT|O_TRUNC,0664);
if(fd == -1){
perror("open");
return -1;
}
//向文件中写入数据
char* buf = "铁锅炖大鹅";
ssize_t size = write(fd,buf,strlen(buf));
if(size == -1){
perror("write");
return -1;
}
printf("实际向文件中写入%ld个字节的数据\n",size);
//关闭文件
close(fd);
return 0;
}
read
#include<unistd.h>
ssize_t read(int fd,void* buf,size_t count);
-
功能:向指定文件读取数据
-
参数:
- fd 文件描述符
- buf 内存缓冲区,存取读到的的数据
- count 期望读取的字节数
-
返回值:成功返回实际写入的字节数,失败返回-1
//读取文件
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
int main(void){
//打开文件
int fd = open("./shared.txt",O_RDONLY);
if(fd == -1){
perror("open");
return -1;
}
//读取文件
//char* buf = NULL; // char* buf = "hello";
char buf[64] = {};
ssize_t size = read(fd,buf,sizeof(buf)-1);
if(size == -1){
perror("size");
return -1;
}
printf("实际读取到%ld个字节的数据\n",size);
printf("%s\n",buf);
//关闭文件
close(fd);
return 0;
}
lseek
lseek
设置文件位置为给定的偏移 offset,参数 offset 意味着从给定的 whence 位置查找的字节数。
文件读写位置通常是一个非负的整数,用off_t类型表示,在32位系统上定义为long int,在64位系统中被定义为long long int
打开一个文件时,除非指定了O_APPEND标志,否则文件读写位置一律被设为0,即文件首字节的位置
每一次读写操作都从当前的文件读写位置开始,并根据所读写的字节数,同步增加文件读写位置,为下一次读写做好准备
因为文件读写位置是保存在文件表项而不是v节点中的,因此通过多次打开同一个文件得到多个文件描述符,各自拥有各自的文件读写位置
#include<unistd.h>
off_t lseek(int fd,off_t offset,int whence);
- 功能:人为调整文件读写位置
- 参数:
fd
: 文件描述符offset
: 文件读写位置偏移字节数whence
: offset参数的偏移起点,可以取如下值:SEEK_SET
- 文件开头SEEK_CUR
- 当前位置开始(最后一个被读写字节的位置+1)SEEK_END
- 文件末尾(最后一个字节位置+1)
- 返回值:成功返回调整后的文件读写位置,失败返回-1
lseek函数的功能仅仅是修改保存在文件表项中的文件读写位置,并不实际引发任何I/O动作
lseek(fd,-7,SEEK_CUR); //返回当前位置向文件头偏移7字节的位置
lseek(fd,0,SEEK_CUR); //返回当前文件读写位置
lseek(fd,0,SEEK_END); //返回文件总字节数
//文件读写位置
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>
int main(void){
//打开文件 0
int fd=open("./lseek.txt",O_WRONLY|O_CREAT|O_TRUNC,0664);
if(fd == -1){
perror("open");
return -1;
}
//像文件中写入数据 hello world! 12
char* buf = "hello world!";
if(write(fd,buf,strlen(buf)) == -1){
perror("write");
return -1;
}
//修改文件读写位置
if(lseek(fd,-6,SEEK_END) == -1){
perror("lseek");
return -1;
}
//再次写入数据 linux!
buf = "linux!";
if(write(fd,buf,strlen(buf)) == -1){
perror("write");
return -1;
}
//再次修改文件读写位置
if(lseek(fd,8,SEEK_END) == -1){
perror("lseek");
return -1;
}
//第三次写入数据
buf = "铁锅炖大鹅";
if(write(fd,buf,strlen(buf)) == -1){
perror("write");
return -1;
}
//关闭文件
close(fd);
return 0;
}
如果通过lseek函数将文件读写位置设置到文件尾,在超过文件尾的位置上写入数据(这部分数据不在文件分配的空间内,所以不会保存),将在文件中形成文件空洞。
文件空洞不占用磁盘空间的原因是因为文件空洞只是文件系统中的一种现象,它并不实际占用磁盘空间。文件空洞是由于在文件中进行写操作时,如果文件已经分配了足够的空间来保存新写入的数据,但实际上并没有将数据写入到所有分配的空间中,那么这些未写入数据的空间就被称为文件空洞。这些空洞只是文件系统中的一种逻辑结构,并不会占用实际的磁盘空间。
文件出现空洞现象后,可能会导致文件无法正常使用,或者出现错误。如果文件上出现了一个小的窟窿,一般来说不会影响文件的使用,但是如果窟窿比较大或者影响到文件内容的完整性,可能会导致文件无法使用或者出现错误。因此,在使用文件前,建议先检查文件的完整性和准确性,确保文件可以正常使用。如果文件出现了问题,可以尝试使用备份文件或者重新制作文件。
IO效率
- 系统调用I/O:系统调用I/O即文件I/O又称为无缓冲IO,低级磁盘I/O,遵循POSIX相关标准。任何兼容POSIX标准的操作系统上都支持文件I/O。
- 标准I/O:标准I/O是ANSI C建立的一个标准I/O模型,又称为高级磁盘I/O,是一个标准函数包和stdio.h头文件中的定义,具有一定的可移植性。标准I/O库处理很多细节。例如缓存分配,以优化长度执行I/O等。标准的I/O提供了三种类型的缓存(行缓存、全缓存和无缓存)。
Linux 中使用的是GLIBC
,它是标准C库的超集。不仅包含ANSI C中定义的函数,还包括POSIX标准中定义的函数。因此,Linux 下既可以使用标准I/O,也可以使用文件I/O。
缓存是内存上的某一块区域。缓存的一个作用是合并系统调用,即将多次的标准IO操作合并为一个系统调用操作。
文件IO不使用缓存,每次调用读写函数时,从用户态切换到内核态,对磁盘上的实际文件进行读写操作,因此响应速度快,坏处是频繁的系统调用会增加系统开销(用户态和内核态来回切换),例如调用write
写入一个字符时,磁盘上的文件中就多了一个字符。
标准IO使用缓存,未刷新缓冲前的多次读写时,实际上操作的是内存上的缓冲区,与磁盘上的实际文件无关,直到刷新缓冲时,才调用一次文件IO,从用户态切换到内核态,对磁盘上的实际文件进行操作。因此标准IO吞吐量大,相应的响应时间比文件IO长。但是差别不大,建议使用标准IO来操作文件。
两种IO可以相互转化:
fileno
:返回结构体FILE的成员_file
,即文件描述符。标准IO->文件IO
int fileno(FILE *stream);
fdopen
:通过文件描述符fd,返回FILE结构体。文件IO->标准IO
FILE *fdopen(int fd, const char *mode);
注意:即使对同一个文件,也不要混用两种IO,否则容易发生错误。
原因:FILE结构体的pos
和进程中的结构体的pos
基本上不一样。
FILE *fp;
// 连续写入两个字符
fputc(fp) -> pos++
fputc(fp) -> pos++
但是,进程维护的结构体中的pos
并未加2;只有刷新缓冲区时,该pos
才会加2;
代码示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
putchar('a');
write(1, "b", 1);
putchar('a');
write(1, "b", 1);
putchar('a');
write(1, "b", 1);
exit(0);
}
打印结果:
PLAINTEXT
bbbaaa
解析:遇到文件IO则立即输出,遇到标准IO,则需要等待缓冲区刷新的时机,这里是进程结束后,进行了强制刷新,将3个a字符输出到终端上。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
putchar('a');
fflush(stdout);
write(1, "b", 1);
putchar('a');
fflush(stdout);
write(1, "b", 1);
putchar('a');
fflush(stdout);
write(1, "b", 1);
exit(0);
}
打印结果:
PLAINTEXT
ababab
strace
命令能够显示所有由用户空间程序发出的系统调用。
以上面第一个程序为例:
PLAINTEXT
strace ./ab
打印结果:
BUFSIZE对IO效率的影响
图中用户CPU时间是程序在用户态下的执行时间;系统CPU时间是程序在内核态下的执行时间;时钟时间是两个时间的总和;
BUFSIZE受栈大小的影响;此测试所用的文件系统是Linux ext4文件系统,其磁盘块长度为4096
字节。这也证明了图中系统 CPU 时间的几个最小值差不多出现在BUFFSIZE 为4096 及以后的位置,继续增加缓冲区长度对此时间几乎没有影响。
当系统调用函数被执行时,需要在用户态和内核态之间来回切换,因此频繁执行系统调用函数会严重影响性能
标准库做了必要的优化,内部维护一个缓冲区,只在满足特定条件时才将缓冲区与系统内核同步,借此降低执行系统调用的频率,减少进程在用户态和内核态之间来回切换的次数,提高运行性能
文件其他操作
dup
#include<unistd.h>
int dup(int oldfd);
- 功能:复制文件描述符表的特定条目到最小可用项
- 参数:oldfd:源文件描述符
- 返回值:成功返回目标文件描述符,失败返回-1
dup函数将oldfd参数所对应的文件描述符表项复制到文件描述符表第一个空闲项中,同时返回该表项对应的文件描述符。dup函数返回的文件描述符一定是调用进程当前未使用的最小文件描述符
dup函数只复制文件描述符表项,不复制文件表项和v节点,因此该函数所返回的文件描述符可以看作是参数文件描述符oldfd的副本,他们标识同一个文件表项
注意:当文件关闭时,即使由dup函数产生的文件描述符副本也应该通过close函数关闭,因为只有当关联于一个文件表项的所有文件描述符都被关闭了,该文件表项才会被销毁,类似地,也只有当关联于同一个v节点的所有文件表项都被销毁了,v节点才会被从内存中删除,因此从资源合理利用角度讲,凡是明确不再继续使用的文件描述符,都应该尽可能及时地使用close函数关闭
dup函数返回的文件描述符与作为参数传递给该函数的文件描述符标识的是同一个文件表项,而文件读写位置是保存在文件表项而非文件描述符表项中,因此通过这些文件描述符中的任何一个对文件进行读写或随机访问都会影响其他文件描述符操作的文件读写位置,这个与多次通过open函数打开同一个文件不同
dup2
#include<unistd.h>
int dup2(int oldfd,int newfd);
- 功能:复制文件描述符表的特定条目到指定项
- 参数:oldfd:源文件描述符
- newfd:目标文件描述符
- 返回值:成功返回目标文件描述符(newfd),失败返回-1
dup2函数在复制由oldfd参数所标识的源文件描述符表项时,会先检查由newfd参数所标识的目标文件描述符表项是否空闲,如果空闲就复制,否则会先将目标文件描述符newfd关闭,使之空闲之后再复制
//文件描述符的复制
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>
int main(void){
//打开文件,得到文件描述符 oldfd
int oldfd = open("./dup.txt",
O_WRONLY|O_CREAT|O_TRUNC,0664);
if(oldfd == -1){
perror("open");
return -1;
}
printf("oldfd = %d\n",oldfd);
//复制oldfd得到newfd
//int newfd = dup(oldfd);
int newfd = dup2(oldfd,STDOUT_FILENO);
if(newfd == -1){
perror("dup2");
return -1;
}
printf("newfd = %d\n",newfd);
//通过oldfd向文件写入数据 hello world!
char* buf = "hello world!";
if(write(oldfd,buf,strlen(buf)) == -1){
perror("write");
return -1;
}
//通过newfd修改文件读写位置
if(lseek(newfd,-6,SEEK_END) == -1){
perror("lseek");
return -1;
}
//通过oldfd再次向文件写入数据 linux!
buf = "linux!";
if(write(oldfd,buf,strlen(buf)) == -1){
perror("write");
return -1;
}
//关闭文件
close(oldfd);
close(newfd);
return 0;
}
输出重定向
将printf
输出结果从屏幕重定向到文件中
- 方法1
close
:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#define FNAME "/tmp/out"
int main(void) {
int fd;
// 关闭stdout,使描述符1空闲
if (close(STDOUT_FILENO) == -1) {
perror("close()");
return -1;
}
if((fd = open(FNAME, O_WRONLY|O_CREAT|O_TRUNC, 0600)) < 0) {
perror("open()");
return -1;
}
printf("Hello World");
return 0;
}
输出结果:
[root@HongyiZeng sysio]# ./dup
[root@HongyiZeng sysio]# cat /tmp/out
Hello World
- 方法2:使用
dup
#define FNAME "/tmp/out"
int main(void) {
int fd;
if((fd = open(FNAME, O_WRONLY|O_CREAT|O_TRUNC, 0600)) < 0) {
perror("open()");
exit(1);
}
// 关闭stdout
close(1);
// 复制fd,让其占据1的描述符
dup(fd);
// 关闭fd
close(fd);
puts("Hello World");
exit(0);
}
图示:
注意结构体中有引用计数,当fd=3
被关闭时,还有fd=1
指向这个结构体,因此结构体不会被销毁掉。存在并发问题。
- 方法3:使用
dup2
#define FNAME "/tmp/out"
int main(void) {
int fd;
if((fd = open(FNAME, O_WRONLY|O_CREAT|O_TRUNC, 0600)) < 0) {
perror("open()");
exit(1);
}
// 如果fd = 1,则什么也不做,返回fd
// 如果fd != 1,则关闭1指向的结构体,再打开1,指向fd的结构体,返回1
dup2(fd, 1);
if(fd != 1) {
close(fd);
}
puts("Hello World");
exit(0);
}
dup2
是一个原子操作,相当于:
dup2(fd, 1);
// 相当于:
close(1);
dup(fd);
- 方法4
freopen
#include <stdio.h>
int main() {
FILE *fp;
// 使用freopen将标准输出重定向到"output.txt"文件
fp = freopen("output.txt", "w", stdout);
// 输出内容,这些内容将被写入到"output.txt"文件中
printf("Hello, World!\n");
printf("This is a test.\n");
// 关闭文件
fclose(fp);
return 0;
}
/dev/fd目录
对于每个进程,内核都提供有一个特殊的虚拟目录/dev/fd
。
该目录中包含/dev/fd/n
形式的文件名,其中n是与进程中打开文件描述符相对应的编号。也就是说,/dev/fd/0
就对应于进程的标志输入。
打开/dev/fd目录中的一个文件等同于复制对应的文件描述符,所以下面两行代码是等价的:
fd = open("/dev/fd/1", O_WRONLY);
// 等价于:
fd = dup(1);
fcntl和ioctl
fcntl
针对文件描述符提供控制。
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
- 返回值:若成功,则依赖于
cmd
,若失败,则返回-1
- 函数功能:
- 复制一个已有的描述符(
cmd=F_DUPFD或F_DUPFD_CLOEXEC
) - 获取/设置文件描述符标志(
cmd=F_GETFD或F_SETFD
) - 获取/设置文件状态标志(
cmd=F_GETFL或F_SETFL
) - 获取/设置异步I/O所有权(
cmd=F_GETOWN或F_SETOWN
) - 获取/设置记录锁(
cmd=F_GETLK、F_SETLK或F_SETLKW
)
- 复制一个已有的描述符(
ioctl
:用于控制设备
#include <sys/ioctl.h>
int ioctl(int d, int request, ...);
ioctl
函数一直是IO操作的杂物箱。不能用本章中其他函数表示的I/O操作通常都能用ioctl表示。终端I/O是使用ioctl最多的地方。
访问测试
#include<unistd.h>
int access(char const* pathname,int mode);
- 功能:判断当前进程是否可以对某个给定的文件执行某种访问
- 参数:pathname 文件路径
- mode 被测试权限,可以取以下值
- R_OK - 是否可读
- W_OK - 是否可写
- X_OK - 是否可执行
- F_OK - 是否存在
- mode 被测试权限,可以取以下值
- 返回值:成功返回0,失败返回-1
修改文件大小
#include<unistd.h>
int truncate(char const*path,off_t length);
int ftruncate(int fd,off_t length);
- 功能:修改指定文件的大小
- 参数:path 文件路径
- length 文件大小
- fd 文件描述符
- 返回值:成功返回0,失败返回-1
该函数既可以把文件截短,也可以把文件扩长,所有改变均发生在文件的尾部,新增的部分用数字0填充
//修改文件大小
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>
int main(void){
//打开文件
int fd = open("./trunc.txt",
O_WRONLY|O_CREAT|O_TRUNC,0664);
if(fd == -1){
perror("open");
return -1;
}
//向文件中写入数据 abcde
char* buf = "abcde";
if(write(fd,buf,strlen(buf)) == -1){
perror("write");
return -1;
}
//修改文件大小
if(truncate("./trunc.txt",3) == -1){
perror("truncate");
return -1;
}
//再次修改文件大小
if(ftruncate(fd,5) == -1){
perror("ftruncate");
return -1;
}
//关闭文件
close(fd);
return 0;
}
文件锁
读写冲突
- 如果两个或两个以上的进程同时向一个文件的某个特定区域写入数据,那么最后写入文件的数据极有可能因为写操作的交错而产生混乱
- 如果一个进程写而其他进程同时在读一个文件的某个特定区域,那么读出的数据极有可能因为读写操作的交错而不完整
- 多个进程同时读取文件的某个特定区域不会产生问题
- 因此为了避免读写同一个文件的同一个区域时发送冲突,进程之间应该遵循以下规则:
- 如果一个进程正在写入,那么其他进程既不能读也不能写
- 如果一个进程正在读取,那么其他进程不能写入但是可以读取
//写入冲突演示,开两个终端同时执行
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>
int main(int argc,char* argv[]){
// ./a.out hello
//打开文件
int fd = open("./conflict.txt",
O_WRONLY|O_CREAT|O_APPEND,0664);
if(fd == -1){
perror("open");
return -1;
}
//向文件中写入数据
for(int i = 0;i < strlen(argv[1]);i++){
if(write(fd,&argv[1][i],sizeof(argv[1][i])) == -1){
perror("write");
return -1;
}
sleep(1);
}
//关闭文件
close(fd);
return 0;
}
-
文件锁分为两种:
-
读锁:对一个文件的特定区域可以加多把读锁
-
写锁:对一个文件的特定区域只能加一把写锁
-
一个文件是可以加多把写锁的,前提是不同写锁的区域不能重叠
-
-
基于锁的操作模型是:读/写文件钟的特定区域之前,先加上读/写锁,锁成功了再读/写,读/写完成以后再解锁
读锁 | 写锁 | |
---|---|---|
无锁 | OK | OK |
多把读锁 | OK | NO |
一把写锁 | NO | NO |
fcntl函数
#include<fcntl.h>
int fcntl(int fd,F_SETLK/F_SETKW,struct flock* lock);
struct flock{
short l_type; //锁类型:F_RDLCK、F_WRLCK、F_UNLCK
short l_whence; //锁区偏移起点:SEEK_SET、SEEK_CUR、SEEK_END
off_t l_start; //锁区偏移字节数
off_t l_len; //锁区字节数,取0表示到文件尾
pid_t l_pid; //加锁进程的PID,-1表示自动设置
}
- 功能:加解锁
- 参数:
F_SETLK
非阻塞模式加锁F_SETLKW
阻塞模式加锁lock
对文件要加的锁,结构体
- 返回值:成功返回0,失败返回-1
//通过文件锁解决写冲突
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>
#include<errno.h> // int errno
int main(int argc,char* argv[]){
// ./a.out hello
//打开文件
int fd = open("./conflict.txt",
O_WRONLY|O_CREAT|O_APPEND,0664);
if(fd == -1){
perror("open");
return -1;
}
/*阻塞方式枷锁
struct flock l;
l.l_type = F_WRLCK;//写锁
l.l_whence = SEEK_SET;
l.l_start = 0;
l.l_len = 0;//一直锁到文件尾
l.l_pid = -1;
if(fcntl(fd,F_SETLKW,&l) == -1){
perror("fcntl");
return -1;
}*/
//非阻塞方式加锁
while(fcntl(fd,F_SETLK,&l) == -1){
if(errno == EACCES || errno == EAGAIN){
printf("文件被锁定,干点别的区\n");
sleep(1);
}else{
perror("fcntl");
return -1;
}
}
//向文件中写入数据
for(int i = 0;i < strlen(argv[1]);i++){
if(write(fd,&argv[1][i],sizeof(argv[1][i])) == -1){
perror("write");
return -1;
}
sleep(1);
}
//解锁
struct flock ul;
ul.l_type = F_UNLCK;//解锁
ul.l_whence = SEEK_SET;
ul.l_start = 0;
ul.l_len = 0;//一直锁到文件尾
ul.l_pid = -1;
if(fcntl(fd,F_SETLK,&ul) == -1){
perror("fcntl");
return -1;
}
//关闭文件
close(fd);
return 0;
}
- 当通过close函数关闭文件描述符时,调用进程在该文件描述符上所加的一切锁将自动解除
- 当进程终止时,该进程所有文件描述符上所加的一切锁将被自动解除
- 文件锁仅在不同进程之间起作用,同一个进程的不同线程不能通过文件锁解决读写冲突
- 通过
fork/vfork
函数创建的子进程不能继承父进程所加的文件锁 - 通过exec函数创建的新进程,会继承原进程所加的全部文件锁,除非某文件描述符带有FD_CLOEXEC标志
如果进程不遵守先加锁再读写最后解锁的这套协议,无视锁的存在,想读就读,想写就写,即便有锁,也对它起不到任何约束作用。因此这样的锁机制被称为劝谏锁或协议锁
内核结构
-
在Linux内核中,锁表(lock table)通常指的是用于管理文件锁的数据结构。文件锁是一种同步机制,用于防止多个进程同时对同一文件进行冲突操作。
-
在Linux内核中,并没有一个全局统一的锁表。相反,每个打开的文件描述符(
file descriptor
)都有一个与之相关联的文件锁表。这意味着每个文件都有自己的锁表,用于管理对该文件的锁定操作。 -
当一个进程打开一个文件时,内核会为该文件创建一个文件描述符,并在内存中为该文件描述符分配一个文件锁表。该文件锁表用于记录对该文件的锁定状态,包括锁定的区域、锁定的类型(共享锁或独占锁)以及持有锁的进程等信息。
-
因此,可以说每个文件都有自己的锁表,而不是整个系统只有一张锁表。这种设计可以确保对不同文件的锁定操作是独立的,互不干扰。
-
每次对给定文件的特定区域加锁,都会通过
fcntl
函数向系统内核传递flock
结构体- 该结构体中包含了有关锁的一切细节,注入锁类型、锁区的起始位置大小以及加锁进程的PID
-
系统内核会收集所有进程对该文件所加的各种锁,并把这些
flock
结构体中的信息,以表链的形式组织成一张锁表- 锁表的起始地址就保存在该文件的v节点中
-
任何一个进程通过
fcntl
函数对该文件加锁,系统内核都要遍历这张锁表,一旦发现有与要加的锁冲突的情况就阻塞或报错,否则就把要加的锁插入锁表,而解锁过程实际上就是调整或删除锁表中的相应节点
文件属性
#include<sys/stat.h>
int stat(char const* path,struct stat* buf);
int fstat(int fd,struct stat* buf);
int lstat(char const* path,struct stat* buf);
/*lstat()不跟踪符号链接,例如:*/
abt.txt --> xyz.txt //abc.txt是xyz的软链接
stat("abc.txt",buf); //得到xyz.txt文件的元数据
lstat("abc.txt",buf); //得到abc.txt文件的元数据
- 功能:从i节点中提取文件的元数据,即文件的属性信息
- 参数:
- path 文件路径
- buf 文件元数据结构
- fd 文件描述符
- 返回值:成功返回0,失败返回-1
//获取文件的元数据
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/stat.h>// stat
#include<time.h>
//用来完成类型和权限的转换
// hello.c --> stat() --> struct stat s -->
// s.st_mode --> mtos() --> -rw-rw-r--
char* mtos(mode_t m){
static char s[11];
if(S_ISDIR(m)){
strcpy(s,"d");
}else
if(S_ISLNK(m)){
strcpy(s,"l");
}else
if(S_ISSOCK(m)){
strcpy(s,"s");
}else
if(S_ISCHR(m)){
strcpy(s,"c");
}else
if(S_ISBLK(m)){
strcpy(s,"b");
}else
if(S_ISFIFO(m)){
strcpy(s,"p");
}else{
strcpy(s,"-");
}
strcat(s,m & S_IRUSR ? "r" : "-");
strcat(s,m & S_IWUSR ? "w" : "-");
strcat(s,m & S_IXUSR ? "x" : "-");
strcat(s,m & S_IRGRP ? "r" : "-");
strcat(s,m & S_IWGRP ? "w" : "-");
strcat(s,m & S_IXGRP ? "x" : "-");
strcat(s,m & S_IROTH ? "r" : "-");
strcat(s,m & S_IWOTH ? "w" : "-");
strcat(s,m & S_IXOTH ? "x" : "-");
return s;
}
//时间转换
char* ttos(time_t t){
static char time[20];
struct tm* l = localtime(&t);
sprintf(time,"%d-%d-%d %d:%d:%d",l->tm_year+1900,
l->tm_mon+1,l->tm_mday,l->tm_hour,
l->tm_min,l->tm_sec);
return time;
}
int main(int argc,char* argv[]){
// ./a.out hello.c
if(argc < 2){
fprintf(stderr,"用法:./a.out <文件>\n");
return -1;
}
struct stat s;//用来输出文件的元数据
if(stat(argv[1],&s) == -1){
perror("stat");
return -1;
}
printf(" 设备ID:%lu\n",s.st_dev);
printf(" i节点号:%ld\n",s.st_ino);
printf(" 类型和权限:%s\n",mtos(s.st_mode));
printf(" 硬链接数:%lu\n",s.st_nlink);
printf(" 用户ID:%u\n",s.st_uid);
printf(" 组ID:%u\n",s.st_gid);
printf(" 总字节数:%ld\n",s.st_size);
printf(" IO块字节数:%ld\n",s.st_blksize);
printf(" 存储块数:%ld\n",s.st_blocks);
printf(" 最后访问时间:%s\n",ttos(s.st_atime));
printf(" 最后修改时间:%s\n",ttos(s.st_mtime));
printf(" 最后改变时间:%s\n",ttos(s.st_ctime));
return 0;
}
标签:文件,调用,return,int,描述符,Unix,fd,IO,include
From: https://www.cnblogs.com/one-ten/p/18619642