赞
踩
文件 = 文件内容 + 文件属性
要操作文件,就要先打开文件。根据冯诺依曼体系,只能操作内存中的数据。因此要先把文件内容加载到存储器,即内存中。
FILE* fopen(const char *path, const char *mode);
int fclose(FILE *fp);
size_t fwrite(const void *ptr, size_t size, size_t number, FILE *stream);
size_t fread(void *ptr, size_t size, size_t number, FILE *stream);
模式 | 含义 | 文件不存在时 |
---|---|---|
r | 只读 | 报错 |
w | 只写 | 创建文件 |
a | 追加只写 | 创建文件 |
rb | 二进制只读 | 报错 |
wb | 二进制只写 | 创建文件 |
ab | 二进制追加只写 | 创建文件 |
r+ | 读写 | 报错 |
w+ | 读写 | 创建文件 |
a+ | 追加读写 | 创建文件 |
rb+ | 二进制读写 | 报错 |
wb+ | 二进制读写 | 创建文件 |
ab+ | 二进制追加读写 | 创建文件 |
一个文件不能同时读和写。
struct _iobuf {
char *_ptr; //文件输入的下一个位置
int _cnt; //当前缓冲区的相对位置
char *_base; //指基础位置(即是文件的起始位置)
int _flag; //文件标志
int _fileno; //文件描述符id
int _charbuf; //检查缓冲区状况,如果无缓冲区则不读取
int _bufsiz; //文件缓冲区大小
char *_tmpfname;//临时文件名
};
typedef struct _iobuf FILE;
C语言的文件接口也是借助了操作系统的系统调用接口,封装了一层。
#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);
man手册里提供了open的两个函数原型,看起来像是重载,但是C语言使不支持函数重载的。实际上是用到了可变参数。
__fortify_function int open (const char *__path, int __oflag, ...)
标志位 | 含义 |
---|---|
O_RDONLY | 只读 |
O_WRONLY | 只写 |
O_RDWR | 读写 |
O_APPEND | 追加 |
O_CREAT | 创建文件 |
O_TRUNC | 清空文件 |
实际上flag是一个位图,用32个bit位最多表示32种状态。
通过与操作就能判断是什么标志位,然后执行不同的逻辑。
用第一个open接口创建的文件的权限是乱的。需要用第二个接口。
int fd = open("Log.txt", O_WRONLY | O_CREAT, 0666);
mode_t umask(mode_t mask);
但是权限的设置还和权限掩码umask有关。
权限掩码的目的是可以让开发者自定义文件的默认权限。
涉及权限掩码,umask,默认为0002
只关注后三位002,凡是在权限掩码中出现的权限,都不应该在最终权限中出现。
计算规则:最终权限 = 起始权限 & 权限掩码
目录文件 | 普通文件 | |
---|---|---|
umask | 000 000 010 | 000 000 010 |
~mask | 111 111 101 | 110 110 100 |
默认权限 | 111 111 111 | 110 110 110 |
最终权限 | 111 111 101 | 110 110 100 |
open的返回值是文件描述符,用来描述进程打开的每一个文件,是一个非负整数。
Linux下一切皆文件,文件描述符可以用于IO,也可以用来操作设备,充当套接字等。当进程打开一个文件,内核会为这个文件分配一个最小的可用文件描述符。
一个进程会默认打开三个文件流,分别为标准输入,标准输出,标准错误流。
这就是为什么成功打开文件时所得到的文件描述符是从3开始进程分配的。
一个进程可以打开多个文件,操作系统如何管理被打开的文件?
先描述再组织,维护内核数据结构,并用链表等管理起来。
task_struct 里有一个 struct files_struct *files,描述一个进程打开的文件集合
files_struct 里有一个 struct file * fd_array[NR_OPEN_DEFAULT],文件描述符表
元素是struct file,即每一个被打开的文件。
struct files_struct {
atomic_t count; // 记录的是共享文件描述符表的进程数量
// 省略大部分
int next_fd;
struct file * fd_array[NR_OPEN_DEFAULT];
};
可以通过ulimit指令调整文件描述符表的大小
打开一个文件,就是把文件的内容和属性从磁盘加载到内存,创建好file结构体,初始化后,从0号下标开始找,找到最小的可用文件描述符,把地址放入到fd_array中。
open函数打开文件成功时数组当中的指针个数增加,这个指针在数组当中的下标被返回。文件打开失败返回-1,因此,成功打开多个文件时返回的文件描述符就是递进的。
关闭标准流,再打开一个文件,可以实现重定向。或者使用系统接口dup和dup2
dup2(A,B)是将B重定向到A,如dup(fd,1),将原来输出到1中的内容输出到fd中。
umask(0);
int fd = open("Log.txt", O_CREAT | O_RDWR | O_TRUNC, 0666);
dup2(fd, 1); // 用fd替换1
printf("Hello\n");
标准输出的fd是1,错误是2,本质上都是指向显示器,目的是区分程序的错误提示和正常输出。错误可以打印到日志里,输出可以打印到显示器。且输出可以被重定向,错误不可以。
./a.out 1> output.txt 2> error.txt
缓冲区无处不在,文件IO也有缓冲区。
缓冲区就是一段内存,把要向指定流输出的内容先存在这段内存里,时机合适就一次性刷新。
因为内存与外设交互速度是非常慢的,进程先把内容放到缓冲区,就不用一直等待和外设交互,多次存储一次输出也可以减少IO的次数。这样做可以提高整体的效率。
结果:立即打印write,三秒后才打印printf和fprintf。
而在printf和fprintf中加了换行后,会按顺序立即打印出来。
而printf和fprintf肯定都是封装了write,因此可以判断,库中还有缓冲区。这里是语言级别的缓冲区,实际上肯定还有操作系统级别的缓冲区,当刷新用户缓冲区的数据时,并不是直接将数据刷新到外设,而是先刷新到操作系统的缓冲区。
当把fork屏蔽掉之后,没有重复。
显示器是行刷新,磁盘文件是全缓冲。重定向时,三个语言级别函数会内容暂存在语言级别缓冲区,因为不是显示器文件而不能行缓冲,而write是无缓冲,所以顺序最先。
fork创建子进程,父子共享代码,数据以写时拷贝的形式继承,其中包含了语言级别的缓冲区。
进程退出之后,父子进程都要刷新缓冲区,写入到文件中,因此会有重复。
软硬链接是Linux系统中两种不同的链接方式,用于实现文件的共享使用。
硬链接是通过索引节点(index_node)来连接的,一个文件可以有多个硬链接,它们共享同一个inode和数据块。是一个独立文件,有自己的iNode和iNode编号与文件内容,相当于快捷方式。
软链接是通过文件路径来进行连接的,它实际上是一个特殊的文件,其中包含了另一个文件的位置信息。不是独立文件,和目标文件使用同一个iNode。
硬链接和软链接有以下几点区别:
ln命令建立链接,UNlink命令接触链接。-s表示软链接,-h表示硬链接。
在删除了dir中的hello后,test下的硬链接同样能正常运行。删除了两者其中的一个,本质就是减少了一份iNode编号和文件的映射,也就是文件的硬连接数减少。
可以推测mv指令的实现原理。mv之后的iNode数不变。硬链接实际上是一份复制。吗?
实际上是在当前路径下新增一份被硬链接文件的文件名和iNode编号的映射关系。
并且可以看到,随着硬链接增多,被链接文件的硬链接数变成了3,类似引用计数的概念。
那为什么创建普通文件,他的硬连接数默认是1?
因为本身的文件名和iNode有一个映射关系,必定大于等于1。
那为什么目录默认是2?
目录本身是文件,所以会有在当前路径下的文件名和iNode编号的映射,这是1;在进入这个目录后,会有一个与当前这个目录形成映射,才有./exe的这种写法,这是2。
那这个iNode就可以类比成指针。而软链接是一个新的文件,其内容保存的是指向文件的所在路径。
此时dir的硬连接数是2,因为本身dir在当前路径就有一份文件名和iNode编号的映射,进入到dir后,还有一份dir中的 .
在dir1里创建一个目录后,硬链接数多了1,因为目录里一定有一个 … 和dir1链接起来。
所以不去自己硬链接的情况下,可以估算一个目录的下级目录中有多少个子目录,硬连接数-2。
#include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #define _bufferNum 512 // 缓冲区大小 #define NONE_FLUSH 0 #define LINE_FLUSH 1 #define FULL_FLUSH 2 typedef struct _myFile { int _fileno; // 文件描述符 char _buffer[_bufferNum]; // 语言级别缓冲区 int _end; // 缓冲区内容的下标 int _flushWay; // 刷新策略 }myFile; myFile* myFopen(const char* fileName, const char* mode) { assert(fileName && mode); int _mode = O_RDONLY; if(strcmp(mode,"r") == 0) {} else if(strcmp(mode,"w") == 0) { _mode = O_CREAT | O_WRONLY | O_TRUNC; } else if(strcmp(mode,"a") == 0) { _mode = O_CREAT | O_WRONLY | O_APPEND; } int fileno = open(fileName, _mode, 0666); if(fileno < 0) return NULL; myFile* mfp = (myFile*)malloc(sizeof(myFile)); if(mfp == NULL) return NULL; memset(mfp, 0, sizeof(myFile)); mfp->_fileno = fileno; mfp->_flushWay |= LINE_FLUSH; mfp->_end = 0; return mfp; } void myFwrite(const char* buffer, size_t count, myFile* mfp) { assert(buffer && mfp && (count > 0)); // 先把内容写到缓冲区 strncpy(mfp->_buffer + mfp->_end, buffer, count); mfp->_end += count; // 刷新策略 只写了行缓冲 if(mfp->_flushWay &= LINE_FLUSH) { if(mfp->_end > 0 && mfp->_buffer[mfp->_end - 1] == '\n') { write(mfp->_fileno, mfp->_buffer, mfp->_end); mfp->_end = 0; syncfs(mfp->_fileno); } } } void myFflush(myFile* mfp) { assert(mfp); if(mfp->_end > 0) { write(mfp->_fileno, mfp->_buffer, mfp->_end); mfp->_end = 0; syncfs(mfp->_fileno); } } void myFclose(myFile* mfp) { myFflush(mfp); free(mfp); }
通过系统调用把缓冲区内容写到内核里,不代表写到了硬件上。需要用到syncfs,syncfs是系统调用,作用是将文件系统中的所有缓存的修改(包括文件元数据和数据)写入到底层的文件系统,把数据刷到磁盘上。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。