文章目录
- 前言
- 一、文件相关概念与操作
- 1.1 open()
- 1.2 close()
- 1.3 write()
- 1.4 read()
- 1.4 写入的时候先清空文件内容再写入
- 1.5 追加(a && a+)
- 二、文件描述符
- 2.1 文件描述符 fd 0 1 2 的理解
- 2.2 FILE结构体:的源代码
- 三、深入理解文件描述符
- 四、理解一切皆文件
- 五、文件描述符的分配规则
- 六、重定向原理
- 七、dup2–重定向函数
- 7.1 使用dup2完成重定向功能
- 八、极简shell增加重定向的功能
- 8.1 C语言实现简易shell全部源码
- 九、缓冲区
- 十、再次深入理解fd2
- 十一、封装一个简单的文件接口库
前言
我们在平时使用的C/C++/Java的时候,我们所用的文件操作都是封装系统接口来进行供我们操作,我们在使用这些接口,本质上就是在访问硬件,也就是磁盘
- 一个硬件设备是如何被函数接口的调用访问到的呢?
当然是通过操作系统,操作系统是管理硬件设备的,在我们学的C/C++/Java等等语言所封装的文件操作接口,都必须通过操作系统的允许,才可以访问到磁盘这个硬件设备,而操作系统是不相信任何用户的,所以为了能够得到操作系统的允许,我们又必须提供一些系统调用接口,供操作系统和用户打交道
- 当我们在语言层面所使用的文件操作函数接口,本质要访问物理硬件设备磁盘,而访问该磁盘时候,必须要操作系统进行管理,同时操作系统会提供一系列的系统调用供用户去访问操作系统,而这些系统调用接口有很多,我们这里所说的系统调用接口是于文件操作相关的系统调用接口;
一、文件相关概念与操作
-
我们所要知道的是:文件=文件内容+文件属性
-
当一个文件的文件内容为空时, 此文件是否占用磁盘空间?
- 这个答案是肯定的, 即使文件的内容为空, 其实此文件也是占用磁盘空间的, 因为文件并不只有内容, 文件还有属性
关于C语言的文件操作我们这里就不介绍了,下面我直接介绍Linux相关的文件~~
1.1 open()
- 函数原型
-
函数参数解析
pathname
所需打开文件的所在路径flags
需要传入的就是打开文件的选项mode
这个参数指的是打开文件需要修改成什么权限的数值,在我们之前学的权限的时候知道,在Linux下创建文件, 系统会根据umask
值来赋予新创建的文件一个默认的文件权限,所以这个mode
就是通过mode修改权限- 而
open()
接口的返回值, 被称为文件描述符fd
, 可以看作表示一个打开的文件
- open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数,表示创建文件的默认权限,否则,使用两个参数的open。
flag
的参数
O_RDONLY
: 只读打开
O_WRONLY
: 只写打开
O_RDWR
: 读,写打开
O_CREAT
: 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND
: 追加写
O_TRONC
:文件以只读或者只写打开是,清空文件内容;
mode_t
:打开文件的权限,以八进制形式写
- 要实现一个参数实现多个功能就需要位图,flags参数其实需要采用位图的方式传参,也就是说,:Linux操作系统为
flags
参数提供的各种选项其实是表示一个整数二进制不同的位. 一个整数的比特位表示flags
参数中某个选项是否被选中
- 我们可以打开
fcntl.h
来查看定义
vim /usr/include/asm-generic/fcntl.h
- 接下来我们来测试一下
open
如何使用:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
// O_WRONLY 代表只写,如果没有该文件就创建,O_CREAT代表创建文件
// 如果不指定创建文件的权限就会乱码
int fd = open("log.txt", O_WRONLY | O_CREAT);
if(fd < 0)
{
printf("fopen fail!\n");
exit(1);
}
close(fd);
return 0;
}
- 正确的使用方式是加上第三个参数:
int main()
{
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
if(fd < 0)
{
printf("fopen fail!\n");
exit(1);
}
close(fd);
return 0;
}
- 这里虽然加上权限了但是怎么不对?少了个w,这是
umask
在作怪
-
在创建文件的时候,OS会将
指定的权限 - umask
作为实际权限 -
我们可以在程序的前面加上umask(0)即可解决
int main()
{
umask(0);
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
if(fd < 0)
{
printf("fopen fail!\n");
exit(1);
}
close(fd);
return 0;
}
1.2 close()
- 函数原型
-
函数参数解读
- fd为传入一个文件描述符,什么是文件描述符,我们后面讲
1.3 write()
- 函数原型
-
返回值
- 写入成功返回写入成功的字节数,返回0为什么也没有写入,返回-1为写入失败
- 写入成功返回写入成功的字节数,返回0为什么也没有写入,返回-1为写入失败
-
函数参数解读:
- 第一个参数为要传入的文件描述符
- 第二个参数为要传入的字符串
- 第三个参数为要写入的长度
-
函数使用
int main()
{
umask(0);
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
if(fd < 0)
{
printf("fopen fail!\n");
exit(1);
}
const char* buffer = "hello world\n";
int cnt = 5;
while (cnt--) {
write(fd, buffer, strlen(buffer));
}
close(fd);
return 0;
}
- 已经写入指定文件成功~~
1.4 read()
- 函数原型
- 函数参数解读:
从文件描述符中读取
const
的字节的数据存入buf
中
int main()
{
umask(0);
int fd = open("log.txt", O_RDONLY);
if(fd < 0)
{
printf("fopen fail!\n");
exit(1);
}
char buffer[128] = { 0 };
// 从文件中读取内容写入buffer, 并输出
read(fd, buffer, sizeof(buffer) - 1);
printf("%s",buffer);
close(fd);
return 0;
}
- 从文件中读取内容写入buffer, 并输出
1.4 写入的时候先清空文件内容再写入
- 我们可以再加一个选项:
O_TRUNC
的作用就是:打开文件时, 先清空文件内容
int main()
{
umask(0);
// 先清空再写入
int fd = open("log.txt", O_CREAT | O_RDWR | O_TRUNC, 0666);
if(fd < 0)
{
printf("fopen fail!\n");
exit(1);
}
const char* buffer = "hello linux\n";
write(fd, buffer, strlen(buffer));
close(fd);
return 0;
}
1.5 追加(a && a+)
- 使用
O_APPEND
即可完成文件的追加
int main()
{
umask(0);
// 先清空再写入
int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);
if(fd < 0)
{
printf("fopen fail!\n");
exit(1);
}
const char* buffer = "hello linux~~\n";
write(fd, buffer, strlen(buffer));
close(fd);
return 0;
}
只传入
O_APPEND
选项, 不传入O_WRONLY
或O_RDWR
是无法追加写入的, 因为没有写入打开
二、文件描述符
- 我们上面所写的fd为open的返回值再次理解一下
- 我们写写下面的这么一段代码,多次打开文件,查看open返回值
int main()
{
umask(0);
int fd1 = open("log.txt", O_RDWR | O_CREAT, 0666);
int fd2 = open("log.txt", O_RDWR | O_CREAT, 0666);
int fd3 = open("log.txt", O_RDWR | O_CREAT, 0666);
printf("fd1: %d\n", fd1);
printf("fd2: %d\n", fd2);
printf("fd3: %d\n", fd3);
close(fd1);
close(fd2);
close(fd3);
return 0;
}
- 这里我们看到返回值是从3开始的,并且递增连续
-
那么为什么从3开始,0,1,2呢?
-
其实在一个进程运行起来的时候默认会给我们打开3个文件流:
-
fd 0:标准输入 –> 键盘
-
fd 1:标准输出 –> 显示器
-
fd 2:标准错误 –> 显示器
-
2.1 文件描述符 fd 0 1 2 的理解
-
当我们的程序运行起来后,编程了进程之后,默认情况下,OS会帮我们打开三个标准输入输出~
-
其中在Linux上:
0:标准输入,键盘
1:标准输出,显示器
2:标准错误,显示器
- 在C语言上:
stdin:标准输入,键盘
stdout:标准输出,显示器
stderr:标准错误,显示器
- 在stdio.h头文件就可以看到声明
- 本质是
stdin
和stdout
stderr
就是一个变量名,类型为FILE*
而这个FILE
结构体里面有个成员就是fd
,文件描述符; - 就是C语言的
stdin
和stdout
stderr
包含 系统的 0 1 2;
不只是C语言,其他语言都有自己的封装
- 我们也可以验证一下:
int main() {
// C语言会默认打开 stdin, stdout, stderr
printf("stdin-fd: %d\n", stdin->_fileno);
printf("stdout-fd: %d\n", stdout->_fileno);
printf("stderr-fd: %d\n", stderr->_fileno);
return 0;
}
2.2 FILE结构体:的源代码
typedef struct _IO_FILE FILE; //在/usr/include/stdio.h
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;
#if
三、深入理解文件描述符
-
前面我们有一个代码是打开多个文件,它返回的
fd
值是连续递增的,其实本质上就是数组的下标,所以本质上是文件描述符实际上就是某个数组的下标 -
一个进程是可以打开多个文件的. 而操作系统中又存在着许多的进程, 其实也就意味着操作系统中存在的大量的被打开的文件
-
操作系统会对这些大量的被打开的文件进行统一的管理, 会将文件的所有属性描述在一个结构体中, 并将所有的描述着打开文件属性的结构体组织在一起进行管理. 就像管理进程,实际上实在管理进程PCB一样,
-
在Linux系统中, 描述的打开文件属性的结构体叫做:
struct file{};
, 每一个打开的文件都由这样一个结构体维护着, 且结构体之间会构成一个数据结构, 方便操作系统进行管理即打开的文件在操作系统中, 实际上都在一个数据结构中维护着若操作系统将这些数据结构以链表的形式连接起来维护, 那么就会存在这样一个维护打开文件的数据结构
-
其中
file
指针指向一个struct file_struct
结构体变量, 而此结构体变量中存储着一个struct file* fd_array[]
指针数组 -
fd_array[]
指针数组中的每一个空间都存储着一个 struct file* 结构体指针, 指向一个打开的文件 -
进程的PCB中有一个结构体指针变量 指向了一个结构体变量, 此结构体变量中存储着
fd_array[]
数组,fd_array[]
中存储着 描述了打开文件属性的结构体的指针, 其实也就是指向了打开的文件 -
而
fd_array[]
数组的下标, 就是open()
、close()
等系统接口使用的fd
文件描述符. 文件操作的系统接口可以通过fd, 在fd_array[]
数组中找到指定下标存储的指针 再找到指针指向的文件
当你在创建一个新的文件时候,那么操作系统就会给你搞一个
strcut file
, 然后把它存放到fd_array[ ]
数组里,然后把对应的下标返回给上一层用户;那么用户就可以拿到下标,也就是描述符干自己的事了
四、理解一切皆文件
-
我们的计算机中, 有着非常多的I/O硬件设备:磁盘、键盘、显示器、网卡……
-
这些I/O设备想要与操作系统交换数据, 一定有它们自己的读写方式, 并且每种硬件的读写方式是独属于此硬件的,各硬件之间的结构不同, 读写方式当然不可能完全相同
-
每种硬件都有其自己的读写方式, 那么当操作系统需要向这些I/O设备写入数据或需要从这些I/O设备中读取数据时, 操作系统会怎么做呢?
- 这些打开的I/O设备, 在操作系统中也会以
struct file{}
结构体的形式维护着, 并且不同硬件的结构体中还会存在函数指针指向此硬件的各种方法:
- 这些打开的I/O设备, 在操作系统中也会以
Linux操作系统的内存文件系统会对所有设备和打开的文件以一个统一的视角进行组织和管理, 这就是 Linux下一切皆文件
- Linux这种将一切设备和文件都以一个统一的视角(file结构体) 进行组织和管理的做法, 被称为 虚拟文件系统(VFS)
五、文件描述符的分配规则
- 我们可以再次观察下面代码
int main() {
int fd = open("log.txt", O_RDONLY);
if(fd < 0)
{
perror("open fail!\n");
exit(-1);
}
printf("fd: %d\n",fd);
close(fd);
return 0;
}
- 上面也说了,默认是从3开始的012分别被输入输出错误占用了
- 那么我们先关闭0再来看一下,这次分配的fd为什么
int main() {
close(0); // 关闭0号描述符
int fd = open("log.txt", O_RDONLY);
if(fd < 0)
{
perror("open fail!\n");
exit(-1);
}
printf("fd: %d\n",fd);
close(fd);
return 0;
}
可以观察到,文件描述符的分配规则:在
files_struct
数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
六、重定向原理
- 那么我们先关闭1也就是输出
int main() {
umask(0);
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if(fd < 0)
{
perror("open fail!\n");
exit(-1);
}
printf("fd: %d\n",fd);
const char* str = "hello world\n";
write(fd, str, strlen(str));
close(fd);
return 0;
}
- 本来要打印到屏幕上的被写入到了文件中
- 当我们关闭了 1号文件描述符,断开了 fd_arrary 数组元素1号位置,也就是断开了标准输入
struct file
的联系,而当我们再次用open
函数打开一个文件为log.txt
时候,文件描述符分配原则告诉我们,就会分配一个数组1
号位置给该文件log.txt
;一旦我们使用printf
输出时候,就不会显示到屏幕了,而显示到文件;这是因为printf
默认是往便准输入输出内容的,而printf
的标准输入就是stdout
这个变量,而stdout
这个变量就是一个FILE类型的结构体指针,而这个结构体指针里面有一个成员就是文件描述符fd
,而fd就是1号,而这个1号就是指向struct file
这个结构体,这个结构体就是标准输入
七、dup2–重定向函数
- 函数原型
-
函数参数解读
-
主要功能是文件描述符的复制
-
成功返回新文件描述符,失败返回-1
-
-
oldfd:原先的文件描述符
-
newfd:新的文件描述符
-
由dup返回的新文件描述符一定是当前可用文件描述符中的最小数值.
-
用dup 2则可以用newfd参数指定新描述符的数值.如果newfd已经打开,则先将其关闭.如若oldfd等于,则dup 2返回newfd,而不关闭它在进程间通信时可用来改变进程的标准输入和标准输出设备
7.1 使用dup2完成重定向功能
int main() {
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0644 );
if(fd < 0){
perror("open error:");
exit(1);
}
dup2(fd,1); //本应该输出到1的,输出到了fd中
printf("printf: hello world\n");
fprintf(stdout,"fprintf: hello world\n");
fputs("fputs: hello world\n", stdout);
close(fd);
return 0;
}
-
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件
log.txt
当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有:>, >>, < -
那重定向的本质是什么呢?
- 原理就是,把oldfd位置的值,复制给了
newfd
位置的值,这会导致,newfd位置的值和oldfd位置值一样,也就是说,newffd
位置的值,不再指向原来的struct file
,而是指向了oldfd
的struct file
- 原理就是,把oldfd位置的值,复制给了
八、极简shell增加重定向的功能
- 实现
char *checkDir(char commandstr[], enum redir* redir_type)
{
char* start = commandstr;
char* end = commandstr + strlen(commandstr);
//1. 检测commandstr内部是否有 > >> <
while(start < end)
{
if(*start == '>')
{
if(*(start + 1) == '>')
{
*redir_type = REDIR_APPEND;
//细节处理为后续命令行分割做铺垫
*start = '\0';
return start + 2;
}
else
{
*redir_type = REDIR_OUTPUT;
//细节处理为后续命令行分割做铺垫
*start = '\0';
return start + 1;
}
}
else if(*start == '<')
{
*redir_type = REDIR_INPUT;
//细节处理为后续命令行分割做铺垫
*start = '\0';
return start + 1;
}
start++;
}
return NULL;
}
- 主函数
char *filename = checkDir(commondstr, &redir_type);
- 子进程的部分:
注意这里一定要将权限先置成0666在执行,要不然可能会出现权限不够写入错误的问题
if(id == 0)
{
int fd = -1;
if(redir_type != REDIR_NONE)
{
//表示找到了文件,并且重定向类型确定
if(redir_type == REDIR_INPUT)
{
fd = open(filename , O_RDONLY);
dup2(fd, 0);
}
else if(redir_type == REDIR_OUTPUT)
{
fd = open(filename , O_CREAT | O_TRUNC | O_WRONLY, 0666);
dup2(fd, 1);
}
else
{
fd = open(filename , O_CREAT | O_APPEND | O_WRONLY, 0666);
dup2(fd, 1);
}
}
//child
execvp(argv[0], argv);
exit(0);
}
8.1 C语言实现简易shell全部源码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#define SIZE 512
#define ZERO '\0'
#define SEP " "
#define NUM 32
#define SkipPath(p) do{ p += (strlen(p)-1); while(*p != '/') p--; }while(0)
#define SkipSpace(cmd, pos) do{\
while(1){\
if(isspace(cmd[pos]))\
pos++;\
else break;\
}\
}while(0)
#define None_Redir 0
#define In_Redir 1
#define Out_Redir 2
#define App_Redir 3
int redir_type = None_Redir;
char *filename = NULL;
char cwd[SIZE*2];
char *gArgv[NUM];
int lastcode = 0;
void Die()
{
exit(1);
}
const char *GetHome()
{
const char *home = getenv("HOME");
if(home == NULL) return "/";
return home;
}
const char *GetUserName()
{
const char *name = getenv("USER");
if(name == NULL) return "None";
return name;
}
const char *GetHostName()
{
const char *hostname = getenv("HOSTNAME");
if(hostname == NULL) return "None";
return hostname;
}
// 临时
const char *GetCwd()
{
const char *cwd = getenv("PWD");
if(cwd == NULL) return "None";
return cwd;
}
// commandline : output
void MakeCommandLineAndPrint()
{
char line[SIZE];
const char *username = GetUserName();
const char *hostname = GetHostName();
const char *cwd = GetCwd();
SkipPath(cwd);
snprintf(line, sizeof(line), "[%s@%s %s]> ", username, hostname, strlen(cwd) == 1 ? "/" : cwd+1);
printf("%s", line);
fflush(stdout);
}
int GetUserCommand(char command[], size_t n)
{
char *s = fgets(command, n, stdin);
if(s == NULL) return -1;
command[strlen(command)-1] = ZERO;
return strlen(command);
}
void SplitCommand(char command[], size_t n)
{
(void)n;
// "ls -a -l -n" -> "ls" "-a" "-l" "-n"
gArgv[0] = strtok(command, SEP);
int index = 1;
while((gArgv[index++] = strtok(NULL, SEP))); // done, 故意写成=,表示先赋值,在判断. 分割之后,strtok会返回NULL,刚好让gArgv最后一个元素是NULL, 并且while判断结束
}
void ExecuteCommand()
{
pid_t id = fork();
if(id < 0) Die();
else if(id == 0)
{
//重定向设置
if(filename != NULL){
if(redir_type == In_Redir)
{
int fd = open(filename, O_RDONLY);
dup2(fd, 0);
}
else if(redir_type == Out_Redir)
{
int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
dup2(fd, 1);
}
else if(redir_type == App_Redir)
{
int fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);
dup2(fd, 1);
}
else
{}
}
// child
execvp(gArgv[0], gArgv);
exit(errno);
}
else
{
// fahter
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
lastcode = WEXITSTATUS(status);
if(lastcode != 0) printf("%s:%s:%d\n", gArgv[0], strerror(lastcode), lastcode);
}
}
}
void Cd()
{
const char *path = gArgv[1];
if(path == NULL) path = GetHome();
// path 一定存在
chdir(path);
// 刷新环境变量
char temp[SIZE*2];
getcwd(temp, sizeof(temp));
snprintf(cwd, sizeof(cwd), "PWD=%s", temp);
putenv(cwd); // OK
}
int CheckBuildin()
{
int yes = 0;
const char *enter_cmd = gArgv[0];
if(strcmp(enter_cmd, "cd") == 0)
{
yes = 1;
Cd();
}
else if(strcmp(enter_cmd, "echo") == 0 && strcmp(gArgv[1], "$?") == 0)
{
yes = 1;
printf("%d\n", lastcode);
lastcode = 0;
}
return yes;
}
void CheckRedir(char cmd[])
{
int pos = 0;
int end = strlen(cmd);
while(pos < end)
{
if(cmd[pos] == '>')
{
if(cmd[pos+1] == '>')
{
cmd[pos++] = 0;
pos++;
redir_type = App_Redir;
SkipSpace(cmd, pos);
filename = cmd + pos;
}
else
{
cmd[pos++] = 0;
redir_type = Out_Redir;
SkipSpace(cmd, pos);
filename = cmd + pos;
}
}
else if(cmd[pos] == '<')
{
cmd[pos++] = 0;
redir_type = In_Redir;
SkipSpace(cmd, pos);
filename = cmd + pos;
}
else
{
pos++;
}
}
}
int main()
{
int quit = 0;
while(!quit)
{
// 0. 重置
redir_type = None_Redir;
filename = NULL;
// 1. 我们需要自己输出一个命令行
MakeCommandLineAndPrint();
// 2. 获取用户命令字符串
char usercommand[SIZE];
int n = GetUserCommand(usercommand, sizeof(usercommand));
if(n <= 0) return 1;
// 2.1 checkredir
CheckRedir(usercommand);
// 3. 命令行字符串分割.
SplitCommand(usercommand, sizeof(usercommand));
// 4. 检测命令是否是内建命令
n = CheckBuildin();
if(n) continue;
// 5. 执行命令
ExecuteCommand();
}
return 0;
}
九、缓冲区
-
当时我们在写进度条的时候也提到了缓冲区–输出缓冲区,那么这个缓冲区在哪里?为什么要存在?和struct file[缓冲区],两个是一回事吗?
-
我们可以再次写下代码观察:
int main() {
const char *msg0="hello printf\n";
const char *msg1="hello fwrite\n";
const char *msg2="hello write\n";
printf("%s", msg0);
fwrite(msg1, strlen(msg0), 1, stdout);
write(1, msg2, strlen(msg2));
fork();
return 0;
}
- 分别执行了两次,一次是直接输出,第二次是重定向到了文件里,再查看文件里的内容
-
我们发现了奇怪的一幕,为什么通过stdout向屏幕输出的内容在文件中显示了两次,而直接采用文件描述符的方式只有一次
-
我们发现 printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用)。为什么呢?肯定和
fork
有关 。
- 一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲
printf
fwrite
库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲- 而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后
- 但是进程退出之后,会统一刷新,写入文件当中
- 但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据
write
没有变化,说明没有所谓的缓冲
-
综上:
printf fwrite
库函数会自带缓冲区,而write
系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。 -
那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是write没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供
-
其实缓冲区就在FILE结构体中
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
};
-
对于缓冲区的理解:
- 用户级缓冲区
- 解耦
- 提高效率(提高使用者的效率,提高IO的效率)
- 内核级缓冲区
- 用户级缓冲区
-
是什么:缓冲区就是一段内存空间
-
为什么:为上层提高高效的IO体验,间接提高整体效率
-
怎么办?
- 刷新策略
- 立即刷新(fflush(stdout),int fsync(int fd))
- 行刷新(显示器)
- 全缓冲。(缓冲区写满,才刷新—>普通文件)
- 特殊情况
- 进程退出,系统自动刷新
- 强制刷新
- 刷新策略
-
内核策略并不关心用户
十、再次深入理解fd2
- 前面我们没有谈到2号描述符有什么作用,我们接下来就来谈一下~
int main()
{
perror("error!!!!!!");// 打印错误信息
fprintf(stdout, "hello fprintf stdout\n");
fprintf(stderr, "hello fprintf stderr\n");
fprintf(stdout, "hello fprintf stdout\n");
fprintf(stderr, "hello fprintf stderr\n");
fprintf(stdout, "hello fprintf stdout\n");
fprintf(stderr, "hello fprintf stderr\n");
fprintf(stdout, "hello fprintf stdout\n");
fprintf(stderr, "hello fprintf stderr\n");
fprintf(stdout, "hello fprintf stdout\n");
fprintf(stderr, "hello fprintf stderr\n");
fprintf(stdout, "hello fprintf stdout\n");
fprintf(stderr, "hello fprintf stderr\n");
return 0;
}
- 观察到我们重定向的时候只将标准输出重定向到了文件中了,错误没有
- 那么我们想讲1和2分别重定向到一个文件中,一个为
ok.txt
一个为err.txt
- 重定正确写法
./myfile 1>ok.txt
- 分别重定向到两个文件
./myfile 1>ok.log 2>err.log
- 将正确的和错误的分开了
- 那么我们可以将全部的信息重定向到一个文件中
./myfile 1>all.log 2>&1
- 首先将
1
里面的内容变成all.log
,然后再将这里的2&1
也写到2
里面
有这个标准错误就是为了能将正确信息和错误信息分开,方便我们
dbug
十一、封装一个简单的文件接口库
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define LINE_SIZE 1024
#define FLUSH_NOW 1
#define FLUSH_LINE 2 // 行缓冲
#define FLUSH_FULL 4 // 全缓冲
#define FILE_NAME "log.txt"
struct _myFILE
{
unsigned int flags;
int fileno;
// 缓冲区
char cache[LINE_SIZE];
int cap;
int pos; // 下次写入的位置
};
typedef struct _myFILE myFILE;
myFILE* my_fopen(const char *path, const char *flag);
void my_fflush(myFILE *fp);
ssize_t my_fwrite(myFILE *fp, const char *data, int len);
void my_fclose(myFILE *fp);
myFILE* my_fopen(const char *path, const char *flag)
{
int flag1 = 0;
int iscreate = 0;
mode_t mode = 0666;
if(strcmp(flag, "r") == 0)
{
flag1 = (O_RDONLY);
}
else if(strcmp(flag, "w") == 0)
{
flag1 = (O_WRONLY | O_CREAT | O_TRUNC);
iscreate = 1;
}
else if(strcmp(flag, "a") == 0)
{
flag1 = (O_WRONLY | O_CREAT | O_APPEND);
iscreate = 1;
}
else
{}
int fd = 0;
if(iscreate)
fd = open(path, flag1, mode);
else
fd = open(path, flag1);
if(fd < 0) return NULL;
myFILE *fp = (myFILE*)malloc(sizeof(myFILE));
if(!fp) return NULL;
fp->fileno = fd;
fp->flags = FLUSH_LINE;
fp->cap = LINE_SIZE;
fp->pos = 0;
return fp;
}
void my_fflush(myFILE *fp)
{
write(fp->fileno, fp->cache, fp->pos);
fp->pos = 0;
}
ssize_t my_fwrite(myFILE *fp, const char *data, int len)
{
// 写入操作本质是拷贝, 如果条件允许,就刷新,否则不做刷新
memcpy(fp->cache+fp->pos, data, len); //肯定要考虑越界, 自动扩容
fp->pos += len;
if((fp->flags&FLUSH_LINE) && fp->cache[fp->pos-1] == '\n')
{
my_fflush(fp);
}
return len;
}
void my_fclose(myFILE *fp)
{
my_fflush(fp);
close(fp->fileno);
free(fp);
}
int main()
{
myFILE *fp = my_fopen(FILE_NAME, "w");
if(fp == NULL) return 1;
const char *str = "hello bit\n";
int cnt = 10;
char buffer[128];
while(cnt)
{
sprintf(buffer, "%s - %d", str, cnt);
my_fwrite(fp, buffer, strlen(buffer)); // strlen()+1不需要
cnt--;
sleep(1);
my_fflush(fp);
}
my_fclose(fp);
return 0;
}