文章目录
1. C库文件操作函数
1.1 fopen函数
FILE *fopen(const char *path, const char *mode);
参数:
path
:带路径的文件名称(待打开的文件)mode
:打开的模式
返回值:
若打开成功,则返回一个文件流指针,否则就返回一个NULL。
1.2 fread函数
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
参数:
-
ptr
:要将读到的数据保存在哪里去,ptr会保存用户准备的一个空间(缓冲区)的地址
size
:表示块的大小,单位为Bytenmemb
:块的个数e.g.:size = 2,nmemb = 5,则读取的字节数为10个Byte。
比较常见的用法就是令size = 1,nmemb则可以指定任意的块
stream
:文件流指针,表示要从哪里开始读
返回值
返回读到的块的个数
1.3 fwrite函数
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
参数:
ptr
:要写入到文件当中的内容size
:块的大小,单位为Bytenmemb
:块的个数总写入的字节数量 = size * nmemb
stream
:文件流指针,从哪里开始写
返回值:
返回写入块的个数
1.4 fseek函数
int fseek(FILE *stream, long offset, int whence);
参数:
stream
:文件流指针,表示需要重新定位文件指针的文件offset
:偏移量,单位为Byte,是相对于whence而言whence
:重置的文件指针位置,通常有3个选项
- SEEK_SET:表示重置到文件的头部
- SEEK_CUR:表示当前文件流指针的位置
- SEEK_END:表示重置到文件的尾部
1.5 fclose函数
int fclose(FILE *fp);
参数:
fp
:需要关闭的文件流指针
返回值:
成功返回0,失败返回EOF
2. 系统调用文件接口
2.1 open函数
①int open(const char *pathname, int flags);
②int open(const char *pathname, int flags, mode_t mode);
open函数有两个,它们的区别就是当文件存在时用①,当文件不存在时就用②
参数:
pathname
:待打开的文件flag
:是对应打开文件的文件描述符所具有的属性
- O_RDONLY:表示只读
- O_WRONLY:表示只写
- O_RDWR:表示读写
以上三种是互斥的,只能选择一个
- O_CREAT:当文件不存在时则创建(需要告知权限,用8进制来表示,需使用②)
- O_TRUNC:截断文件 (清空文件)
- O_APPEND:追加
以上三种是可以任意组合的,且这些属性的定义都包含在
include<fcntl.h>
库中
mode
:权限码,当文件不存在时使用O_CREAT要创建文件时,需要赋予其对应得权限,使用8进制来表示
组合使用的方式:按位或
例如:
//打开test.c文件,若不存在则创建文件,并赋予权限为rw- rw- r--,并且只能写,且每次写的时候都会截断文件(清空)
open("test.c",O_WRONLY|O_CREAT|O_TRUNC,0664) // 0664 - rw- rw- r--
返回值:
返回一个正整数,其含义为文件描述符
在内核源码中查看flag参数中的属性声明
- 那么这些属性之间连接为什么要进行按位或呢?
将这些属性按照位图的方式来使用,如果想要某一个属性,则使用按位与的方式,将属性对应的比特位设置为1,通过按位或之后就可以拿到对应的属性值。
int fcntl(int fd, int cmd, ... /* arg */ );
该函数可以获取文件描述符的属性,它会将其属性通过返回值返回回来.
- 文件描述符:本质上是一个正整数(小整数)
2.2 read函数
ssize_t read(int fd, void *buf, size_t count);
参数:
fd
:文件描述符buf
:将读到的内容存入 buf 这个缓冲区中count
:表示最大能读多少个字节,和 buf 的大小有关,通常需要在 buf 中预留一个 ‘\0’ 的位置
返回值:
- >0:表示读到的字节数
- -1 :表示读取失败
2.3 write函数
ssize_t write(int fd, const void *buf, size_t count);
参数:
fd
:文件描述符buf
:写入的内容放在 buf 中count
:写入的字节数量
返回值:
- >0:表示写入文件中真实的字节数量
- -1:表示写入失败
2.4 lseek函数
off_t lseek(int fd, off_t offset, int whence);
参数:
fd
:文件描述符offset
和whence
参数均和上面写的 fseek函数 中的参数属性一样。
返回值:
成功完成后,lseek()返回结果偏移位置,以偏移量为单位,从文件开头开始。 错误时,值返回-1,并且将errno设置为指示错误。
2.5 close函数
int close(int fd);
参数:
fd
:待关闭的文件的文件描述符
返回值:
成功返回0,失败返回-1
3.文件描述符和文件流指针
3.1 文件描述符
在说文件描述符之前,我们先来讲讲一个程序在运行时是怎样访问到磁盘上的文件的。这次我们从Linux内核源码上来对其进行刨析。
Linux内核源码的下载与安装,请看:CentOS安装相应版本的内核源码
① 首先,程序在运行时,操作系统为了管理该程序,会产生一个进程来描述它,这时候进程就会产生一个struct task_struct
的结构体,即进程控制块(PCB)。
②在task_struct这个结构体中,具有一个struct files_struct* files
的这样一个结构体指针。
其中,files为指针,并不会指向数组,为的是同进程的线程的 files指向相同 file_struct,这是线程间共享打开的文件的本质。
③而在files_struct这个结构体里,又具有一个struct file __rcu * fd_array[NR_OPEN_DEFAULT]
的结构体类型的指针数组。
④fd_array数组中存放都是一些 struct file __rcu* 。
其中__rcu是一种同步机制,可避免使用锁原语,而多个线程同时读取和更新通过指针链接并属于共享数据结构的元素;对于被RCU保护的共享数据结构,读操作不需要获得任何锁就可以访问,但写操作在访问它时首先拷贝一个副本,然后对副本进行修改,最后在适当的时机把指向原来数据的指针重新指向新的被修改的数据。这个时机就是所有引用该数据的CPU都退出对共享数据的操作。
Linux内核中内存管理大量的运用到了RCU机制。为每个内存对象增加了一个原子计数器用来继续该对象当前访问数。当没有其他进程在访问该对象时(计数器为0),才允许回收该内存。
更为详细请看:READ-COPY-UPDATE
在这里我们不对__rcu做更多的解释,只要把它理解成为是操作系统的一种同步机制就可以了。
因此,fd_array数组中实际存放都是一些 struct file*
。而且fd_array数组的下标就是我们所说的文件描述符了
struct file
:保存的是一些文件元信息,如文件名称、文件大小、文件所有者、文件权限等等,它通过一个struct inode
指针指向磁盘中具体对应的那一块物理内存了,这些,我会在后面在进行详解。
如果感觉关系太乱记不住,没关系,直接看下图:
fd_array[ ]数组下标的0,1,2号文件描述符是固定的标准输入、标准输出和标准错误
小结:
- 文件描述符就是内核当中fd_array[ ]数组的下标,是一个正整数
- 文件描述符分配的规则为最小占用原则
- 一个进程最大可以打开100001(默认的),且是可以修改的,使用
ulimit -n [修改值]
即可可使用
ulimit -a
命令查看``
3.2 文件流指针
同样,这次我们直接从C源码中查看文件流指针FILE到底是个什么。
①我们进入到/user/include/stdio.h
文件中对FILE进行查看,发现它是一个struct _IO_FILE
类型的结构体
再往后看,就会发现我们的三个最常见的文件流指针:标准输入(stdin)、标准输出(stdout)、标准错误(stderr)
②查看struct _IO_FILE,可以发现六个有关读写的指针和一个int _fileno
的整型
_IO_read
开头的是读缓冲区_IO_write
开头的是写缓冲区int _fileno
保存了文件描述符的数值
用一个图来总结一下就是:
和上面的文件描述符对应一下,就可以清晰的看出整个IO的过程
怎么样,是不是很壮观,这就是一个小小的IO体系图
在进程终止章节的时候,我们提到了刷新缓存区的概念,其中这个缓存区就是C库维护的读写缓冲区。
再探_exit函数:
_exit函数之所以不会刷新缓冲区,是因为_exit是内核代码直接对内核进行操作的,进程一结束,并不会通知上层的C库,因此,C库维护的读写缓冲区再毫无感知的情况下就被关闭了。
3.3 区别
- 文件流指针是一个结构体,它在内部保存了文件描述符
- 文件描述符是一个正整数,其含义为fd_array[ ]数组的下标
- 文件流指针维护了读写缓冲区