@[TOC]
前言
- 本篇针对的是对打开文件的描写
文件基础常识
- 文件=文件内容+文件属性
文件内容:就是我们可以看到的文本内容
文件属性:文件的读写,运行权限,创建者,日志信息等
- 无论文件内容还是文件属性都是一种数据,在磁盘上都要占据内存
文件操作
- 文件操作=文件内容操作+文件属性的操作
文件内存操作:对文本内容进行修改等操作
文件属性操作:对文件属性的权限,日志信息等的操作
- 被打开的文件称为内存文件,未打开的称为磁盘文件
打开文件
- 打开文件:是将文件的内容和文件属性加载到内存。由冯诺依曼体系结构决定,cpu直接和内存进行交互。
- 打开文件是根据当前的需要打开某些文件,并不是打开所有文件
- 打开一个文件时就会创建一个文件结构体。
FILE结构体
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
//缓冲区相关
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
最后修改时间
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno; //封装的文件描述符
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
creat
创建文件,并给予初始权限,用8进制码表示,要考虑权限掩码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int creat(const char *pathname, mode_t mode);
open
打开文件
#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 | 打开的文件名+路径 | 路径有相对和绝对路径 |
mode | 初始权限码; | 最终权限=初始权限&(~权限掩码) |
flags | 打开文件的方式 | |
返回值 | 文件描述符 | 成功返回fd,失败返回-1 |
write
从一个buf,向fd写入count个字节
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
返回代表实际读取的字符个数
read
向fd中读取cont字节到buf中
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
进程和文件
- 文件是存放在磁盘,向文件写入的内容,最终会写入到磁盘里面,而只有OS可以操作磁盘,因此在语言层面上我们通过函数接口生成进程后,进程会通过相应的系统接口,让OS与磁盘进行交互。
- 文件的操作最终是进程对文件的操作
- 任何对硬件的操作都要贯穿OS
路径
当前路径指工作路径
相对路径:相比于当前路径的位置:./xx
绝对路径是从根到当前位置的路径
修改路径
chdir:可以修改当前进程的工作路径
cat模拟
封装接口
- 原生系统接口,使用成本非常高
- 直接使用单一系统接口语言,跨平台(window,linux,macos)服务不行。
- c语言解决跨平台问题:条件编译+穷举所有的底层接口
默认流
OS管理文件
- OS要想管理一个对象,必然要先描述这个对象也就是生成特定的内核数据结构体。因此OS管理文件就要先描述文件之后再组织文件。
- 通过文件结构体描述文件,通过链表链接结构体,最终对文件的管理变成了对链表的增删查改。
文件描述符-fd
- 加载到内存中的文件,为了方便标识与管理,OS使用文件描述符fd进行标识内存文件
- 文件结构体中的 pf->_fileno
- 在进程中,通过一个指针数组和文件链表建立映射关系
- 对于指针数组的前3个元素默认是:标准输入,输出,错误流
- 当建立新的映射关系时,OS会遍历这个数组,将文件结构体地址存放在第一个为NULL数组元素,并返回数组下标,这也就是文件描述符为什么是一个整型。
- 当链表删除节点时,对于指针数组,其直接在对应下标赋值为NULL。
c语言实现面向对象类
C语言的结构体中,一般我们只考虑成员变量,但是当我们引入函数指针时,这个结构体就可以模拟类
而一旦我们引入了函数指针,那么我们就可以通过这个指针指向任意的符号要求的函数。
而我们我们外设也就是硬件,它们的读写技术肯定是不一样的,但是如果相同的接口,在函数体中的是读写逻辑,那么我们就可以实现一个函数指针指向多个外设。---这就是C++多态的前身。
linux下一切皆文件
对于硬件层,os总是将其看作文件,对于不同的硬件,只需要提供其特殊的文件读写方式,就可以实现以统一的视角处理硬件层。这称为虚拟文件系统
背景
- 对于不同的外设,他们的读写方式必然不相同,但是函数指针的泛型,这就可以通过同一个文件结构体,描述不同的外设。同时对于OS来说,它通过文件描述符就可以访问不同的外设,即OS以统一的视角管理不同的外设。
- 将外设抽象为一个文件结构体,对这个结构体进行管理和驱动层就可以映射到外设硬件身上。
- 我们称这种管理逻辑系统为虚拟文件系统(VFS)
重定向
- 重定向的本质是对文件描述符指针数组中元素的替换,以完成内存中数据的流出与流入方向的改变。
- 文件描述符的指针数组的前3个元素是默认的文件:标准输入流(0),标准输出流(1),标准错误流(2).
- 重定向后,会改变缓存区的刷新策略
输出重定向
OS总是将fd=0,1,2的文件,认为是标准输入流,标准输出流,标准错误流文件,因此如果我们将fa=1指向的文件改为其它文件,就可以完成输出重定向。
追加重定向
在打开文件时,通过添加O_APPEND即可完成追加重定向。
输入重定向
将fd=0的指向文件修改就可以完成输入重定向
dup
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <fcntl.h> /* Obtain O_* constant definitions */
#include <unistd.h>
int dup3(int oldfd, int newfd, int flags);
- 文件结构体的销毁使用的是“引用计数”方式,销毁也是进程消失后OS进行的销毁
- 我们通过关闭fd为012的指针数组,利用文件描述符的匹配规则,就可以完成重定向操作,但是这很麻烦,可以通过一个系统接口函数dep相关接口,进行操作
- 主使用dup2
dup2
int dup2(int oldfd, int newfd);
//dup2() makes newfd be the copy of oldfd, closing newfd first if necessary, but note the following:
- dup2的修改逻辑是将oldfd拷贝到newfd,与正常逻辑相反
- 因此如果想完成重定向,需要fd(x,0/1/2);
标准输出和标准错误的区别
- 2者都会打开屏幕这个标准输出文件,但是2者的文件描述符是不一样的
- 可以通过 2>&1 ,将标准错误重定向到一个文件中
缓冲区
- 设置缓冲区存在目的:减少IO操作,提高整体的效能
我们所说的缓冲区指的是语言级别的缓冲区、
- 因为和输出相关的函数(printf,fprintf....)都会调用系统接口write,但是write是不受刷新策略的影响,所以这个缓冲区和OS是无关的,一定是在输出函数内部,也就是和语言强相关。这也就是为什么有些时候数据没有成功从缓冲区中刷新出来的原因。
- 这其实是文件结构体中的一个类似容器的成员。
- 当进程退出的时候,会刷新FILE内部的数据到OS中。
- 如果在刷新缓冲区前,我们关闭了输出文件,那么数据是无法刷新到OS的,也就是无法保存到文件中
子进程与缓冲区
进程替换与缓冲区
进程替换替换的是代码和数据,因此原来进程所创建的内核数据结构是不变的:文件描述符指针数组也不变。即如果新的程序需要打开文件,会在原有的内核数据结构上补充内容。