- 介绍
- 文件描述符的概念
- open\close
- read\write\lseek
- 标准IO与系统调用IO的区别
- 其他内容
- dup\dup2
- 文件同步
- fcntl\iocntl
介绍
文件描述符的概念
-
备用图
- 文件是一块磁盘空间,有一个编号
inode
,每次open
一个文件时,会创建一个结构体,链接inode
,存储文件的信息,结构体的首地址以指针的形式(类似于FILE*
)存放在一个数组中,向用户返回数组的下标,这就是文件描述符fd
。因此,拿到下标就能拿到指针,拿到指针就能拿到结构体,从而操作文件。 结构体和数组是在 内存空间 中。 每个进程有各自的数组和结构体 - 由于标准IO是建立在系统调用IO的基础上的,因此
FILE
结构体中必然存在整型成员fd
- 数组的大小是1024个,可以用
ulimit -a
查看,进程打开时,fd
0 1 2 对应stream
stdin stdout stderr,是默认打开的,所以fd
从3开始 - 进程间的链接同一个
inode
的结构体互不影响,进程内链接同一个结构体的fd
也互不影响。 结构体和inode
都有 引用计数 。结构体的引用计数是用来操作打开文件的,记录有多少个fd
l链接向自己,inode
的引用计数是用来决定文件是否存在的(而不是记录有多少个结构体链接向自己):当结构体的引用计数为0时,结构体才会被销毁,所以不会出现把fd4
close后,fd6
变成野指针的情况;当inode
引用计数(初始化为1)为0时,文件会被销毁
软硬链接:
-
硬链接就是 目录项 ,使
inode
引用计数加1;软链接类似于windows中的快捷方式,是另外的一个独立文件,这个文件记录了源文件的路径,从而实现链接。 -
优缺点:硬链接不能跨分区建立,不能给目录建立; 符号链接可以跨分区,可以给分区建立。
-
文件在什么时候会真正从磁盘上被删除?
- 硬链接数 == 0
- 没有打开的文件描述符【没有进程或线程用到该文件】
-
概念:https://xzchsia.github.io/2020/03/05/linux-hard-soft-link/
-
概念练习【关键把握软连接是一个独立的文件】:https://blog.csdn.net/weixin_42306122/article/details/108351874
-
操作:https://www.runoob.com/note/29134
open\close
##include <sys/types.h>
##include <sys/stat.h>
##include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
不是用重载,而使用变参实现
-
参数:
-
pathname
: 路径 -
flags
:位图,用来控制打开方式、文件创建和文件状态O_READONLY
O_WRONLY
和O_RDWR
是必选项
-
-
返回值:成功返回文件描述符,失败返回-1
##include<unistd.h>
int close(int fd);
关闭一个文件描述符
read\write\lseek
##include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);##include <sys/types.h>
##include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
read
:从fd中读count个字节到bufwrite
:将buf中的count个字节写入fd中- 返回值:返回成功读入或写入的字节数,出错返回-1
lseek
:用法同fseek
,成功返回举例开头的字节数,失败返回-1 【等价于fseek
+ftell
】
例子:实现 mycopy.c
示例代码:
##include<stdio.h>
##include<stdlib.h>
##include<sys/types.h>
##include<sys/stat.h>
##include<fcntl.h>
##include<unistd.h>##define BUFSIZE 1024
int main(int argc, char **argv)
{if(argc < 3){fprintf(stderr, "Usage:...\n");exit(1);}size_t sfd, dfd;int len=0, ret, pos;char buf[BUFSIZE];sfd = open(argv[1], O_RDONLY);if(sfd < 0){perror("open()");exit(1);}dfd = open(argv[2], O_WRONLY|O_CREAT|O_TRUNC, 0600);if(dfd < 0){perror("open()");exit(1);}while(1){len = read(sfd, buf, BUFSIZE);if(len < 0){perror("read()");break;}if(len == 0)break;pos = 0;//确保写够len个字节while(len > 0){ret = write(dfd, buf+pos, len);if(ret < 0){perror("write()");break;}pos += ret;len -= ret;}}close(dfd);close(sfd);exit(0);
}
运行结果:
几点说明:
open(argv[2], O_WRONLY|O_CREAT|O_TRUNC, 0600);
:只写打开,有则创建,无则删除,创建的权限为0600
write
的循环是为了确保 写够len个字节,如果只写if(ret<0) {perror(); break;}
会出现读入7byte,写入3byte但不报错的情况。场景:该进程被信号打断,没有写够;对设备进行io。
标准IO与系统调用IO的区别
举例:传达室老大爷跑邮局[拿一封送一封,一起送(缓冲区满)\加急时送(刷新缓冲区)]
区别:响应速度&吞吐量
面试:如何使一个程序变快
提醒:标准IO与系统调用IO不可混用
转换: fileno, fdopen
类型 | 响应时机 | 优势 |
---|---|---|
标准IO | 缓冲区满、刷新缓冲区 | 吞吐量大 |
系统调用IO | 立刻执行 | 响应速度快 |
不可混用的原因:虽然FILE内部包含fd,并且FILE和fd可以通过两个函数转换,但是FILE内的属性如pos和fd指向的结构体内的属性往往是不同的,因为写的时候存在buf,读的时候存在cache。
比如:写10个byte,FILE的pos+10,但是此时缓冲区还没有刷新,所以fd指向的结构体内的pos不变
同理,读1个byte时,FILE的pos+1,但是由于cache存在,可能会预读取,所以fd指向的结构体的pos可能+10
例子: 标准IO用系统调用IO实现,合并系统调用IO
示例代码:
##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);
}
运行结果:
通过指令 strace ./ab
查看系统调用情况
三次调用putchar(’a’)相当于底层调用一次 write(1,”aaa”,3)
其他内容
思路:类似于删除数组的中间元素。将11行后的内容一次搬到10行,知道搬完,最后减去10行的大小。truncate改变文件大小。
通过两次打开同一个文件,简化系统调用,也可以用两个线程或进程
dup\dup2
int dup(int oldfd);
int dup2(int oldfd, int newfd);
dup
:把 oldfd
文件描述符,复制到当前文件描述符数组的最低位。
dup2
:把 oldfd
复制到 newfd
,如果 newfd
已被占用,会把占用 newfd
的文件关闭,等价于 close()+dup()
,由于是原子操作所以可以避免并发竞争
例子:在程序中把标准输出重定向到一个文件中,并且输出
示例代码1:关闭 fd1
,重新打开文件, fd
就会占用1
##include<stdio.h>
##include<stdlib.h>
##include<sys/types.h>
##include<fcntl.h>
##include<unistd.h>##define FNAME "/tmp/out"
int main()
{int fd; close(1);fd = open(FNAME, O_WRONLY|O_CREAT|O_TRUNC, 0666);if(fd < 0){ perror("open()");exit(1);} /***************/puts("Hello"); exit(0);
}
示例代码2:用 dup
实现
##include<stdio.h>
##include<stdlib.h>
##include<sys/types.h>
##include<fcntl.h>
##include<unistd.h>##define FNAME "/tmp/out"
int main()
{int fd; //close(1);fd = open(FNAME, O_WRONLY|O_CREAT|O_TRUNC, 0666);if(fd < 0){ perror("open()");exit(1);} close(1);dup(fd);/***************/puts("Hello"); exit(0);
}
示例代码3:用 dup2
实现,避免并发竞争
##include<stdio.h>
##include<stdlib.h>
##include<sys/types.h>
##include<fcntl.h>
##include<unistd.h>##define FNAME "/tmp/out"
int main()
{int fd; //close(1);fd = open(FNAME, O_WRONLY|O_CREAT|O_TRUNC, 0666);if(fd < 0){ perror("open()");exit(1);} dup2(fd,1);if(fd != 1)close(fd);/***************/puts("Hello"); exit(0);
}
示例代码1、2都有并发问题:如代码2中, close(1)
之后,兄弟线程创建了一个文件,就占用了 fd1
,所以 dup(fd)
占用的就再是 fd1
了。
示例代码3注意关闭 fd
内存泄漏:同时注意, if(fd != 1)
是为了防止一开始 fd1
就是空缺的,这样 fd=1
,虽然 dup2(fd,1)
在oldfd==newfd的时候do nothing,如果不加判断,就是把 fd
关闭,导致后面的 puts
无法输出。
其他问题:程序结束后应该还原 fd1
到标准输出。
运行结果:
要当自己在写一个模块,考虑前后文以及与其他模块的并发,而不仅仅是写一个main函数
文件同步
将 buffer 和 cache 中的内容同步到 disk(磁盘)
sync fsync fdatasync
fcntl\iocntl
int fcntl(int fd, int cmd, ... /* arg */ );
一切关于文件描述符的魔术,都来源于该函数。 管家级函数.
参数: cmd
指命令,不同的命令有不同的参数,返回值的含义也不同
ioctl
:管家级别的函数。用来管理设备。【一切皆文件的理念的受害者,因为文件只有open\close\write\read\lseek五种基本操作,但是设备显然不仅仅如此,于是都交给 ioctl
来管, man ioctl_list
】
用 fcntl
设置对 fd
的IO为非阻塞:
/dev/fd
:虚目录,显示的是当前进程文件描述符的信息。
相当于照镜子:谁全查看反映的就是谁,所以这里显示的是 ls
进程使用的文件描述符。
在程序中要得到当前程序所在进程使用的文件描述符,只需要在程序中打开该目录即可。