为啥访问一个文件是进程在访问呢?来看一段代码
#include <stdio.h>
int main()
{
FILE *fp = fopen("log.txt", "w");
if(fp == NULL)
{
perror("fopen");
return 1;
}
const char *message = "hello file\n";
int i = 0;
while(i < 5)
{
fputs(message, fp);
i++;
}
fclose(fp);
return 0;
}
结果如下:
我们可以发现:
💫 那么我们现在有个问题,我们编好了代码,这个文件是不是就打开了 -- 没有,因为我们把代码写好之后,这个还只是一个文本,那是不是把代码编译成可执行程序,文件就打开了 -- 答案也是没有的,把原代码编译成可执行程序仅仅是跑起来了。
🌈 那么什么时候文件才真正被打开呢?
进程 是在 内存 当中的,进程加载到内存中,最终是由 CPU 去执行,可是进程要进行文件读取操作时,这个文件是在磁盘上的,它们又是咋联系上的呢?
结论:访问一个文件之前必须先打开它,根据冯诺依曼,无法访问磁盘上的文件,必须加载到内存上
如何管理文件?
结论:我们研究打开的文件,就是在研究 进程 和 文件 的关系
我们上面 fopen 中的 'w' 是 覆盖式写入,会将文件清空之后再写入。这个就类似于 我们之前学的
这个 > 就叫作 输出重定向,写入前把文件先清空。
案例:给上面代码加个 字符数组
int main()
{
FILE *fp = fopen("log.txt", "w");
if(fp == NULL)
{
perror("fopen");
return 1;
}
char buffer[1024];
const char *message = "hello file";
int i = 0;
while(i < 5)
{
snprintf(buffer, sizeof(buffer), "%s:%d\n", message, i);
fputs(buffer, fp);
i++;
}
fclose(fp);
return 0;
}
输出如下:
追加写入 -- a
同样在 echo 命令中 我们也可以用 >> 来追加式写入
概念补充:任何一个程序在启动之前默认需要打开三个流
但是键盘、显示器不是属于硬件嘛,怎么跟文件流有关系,这个和我们之前学的 Linux 下一切皆文件有关(TODO)
一个程序启动时会打开三个流,而其中 C 语言底层所对应的硬件时键盘、显示器,但是它把这个键盘、显示器包装成了文件的样子,最后就可以 File* 的形式来访问文件了。
那么现在有个问题是谁默认打开这三个流的呢?
把打印内容到显示器的 三种方法
#include <stdio.h>
int main()
{
printf("hello world\n");
fputs("aaaa", stdout);
fwrite("bbbb", 1, 4, stdout);
fprintf(stdout, "cccc");
return 0;
}
① pathname: 要打开或创建的目标文件 ② flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags(本质是个 宏) 参数说明:
③ 参数mode 组合 此为Linux2.2以后特有的旗标,以避免一些系统安全问题。参数mode 则有下列数种组合,只有在建立新文件时才会生效,此外真正建文件时的权限会受到umask值所影响,因此该文件权限应该为(mode-umaks) ④ 返回值 若所有欲核查的权限都通过了检查则返回文件描述符,表示成功,只要有一个权限被禁止则返回-1。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
open("log.txt", O_WRONLY|O_CREAT);
return 0;
}
运行上面代码,发现创建了log.txt,但是它的权限是乱码的。这是因为我们在最初的时候并没有给其分配权限,修改如下:
open("log.txt", O_WRONLY|O_CREAT, 0666);
此时权限就正常了,但是我们明明指定的权限明明是 666 ,但是这上面为啥显示的是 664 呢,因为系统存在 umask(0002)的默认权限掩码,权限掩码会与我们设置的权限进行位运算。那么我们应该怎么做,才能不让其去掉这个权限呢?如下:
此时将代码中的 umask 设置为对应的 0 后,权限掩码就不会给我们去掉 默认的 umask (0002)了,结果就对上了
注意:权限掩码按照就近原则,如果我们有设置默认权限掩码,就用我们设置的,如果没有,就会使用系统默认的。
int main()
{
int fd1 = open("log.txt", O_WRONLY|O_CREAT, 0666);
if(fd1 < 0)
{
perror("open");
return 1;
}
printf("fd1: %d\n", fd1);
const char* message = "hello world\n";
write(fd1, message, strlen(message));
close(fd1);
return 0;
}
上面是系统调用接口close和write。fd就是open的返回值。
输出如下:
我们把message里的内容换成aaa,然后直接运行代码。
const char* message = "aaa";
发现之前的内容还在,旧的内容没被完全清空:
如果我们想像C语言fopen的“w”打开方式一样, 打开就清空文件,就需要再传 O_TRUNC
表示 如果文件已经存在,而且是个常规文件,并以写的方式打开,传入这个选项后,他就会把文件清空。
补充: 我们还可以用 O_APPEND 来对内容进行 追加式写入
还记得我们上面写的 fd 作返回值嘛,在认识返回值之前,先来认识一下两个概念:系统调用和库函数
系统调用接口和库函数的关系,一目了然。 所以,可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。
举个例子:
int main()
{
int a = 12345;
write(1, &a, sizeof(a));
return 0;
}
经过输出,我们发现最后输出结果不是 12345
解决如下:
int main()
{
int a = 12345;
char buffer[1024];
snprintf(buffer, sizeof(buffer), "%d", a);
write(1, buffer, strlen(buffer));
return 0;
}
🍋 因此我们可以知道直接把数字打印到显示器用系统调用接口是不行的,必须做相关的转化变成相关字符然后依次地达到显示器上。
🍎 那么我们有个问题,我们已经有了对应的 read 接口向显示器写,为啥还需要提供这么多写入接口呢?
因为很多情况下需要把我们内存级别的二进制数据转化成字符串风格,然后通过 write 打印到显示器上,这个就叫作 格式化 的过程,然后由于系统调用,这个需要用户自己来实现,为了方便,就提供了这些接口.
输出如下:
文件描述符就是一个小整数 open 的返回值 fd 是从 3 开始的。因为C语言默认会打开三个输入输出流,
情况一: write 向 1 输出
可以用write配合文件描述符在显示器上打印 ---
int main()
{
const char *message = "hello write\n";
write(1, message, strlen(message)); // 默认提供的
}
// 输出描述:
[lighthouse@VM-8-10-centos File-IO]$ ./filecode
hello write
情况二:read 向 0 读取
int main()
{
char buffer[128];
ssize_t s = read(0, buffer, sizeof(buffer));
if(s > 0){
buffer[s - 1] = 0; // 吞掉最后一个换行符
printf("%s\n", buffer);
}
return 0;
}
// 输出描述:
[lighthouse@VM-8-10-centos File-IO]$ ./filecode
abcd
abcd
情况三:把字符串 \0 写入文件
int main()
{
int fd1 = open("log.txt", O_WRONLY| O_CREAT | O_APPEND, 0666);
const char* message = "aaa\n";
write(fd1, message, strlen(message) + 1);
close(fd1);
return 0;
}
我们会发现这样的结果,这个是为什么呢? --》 字符串以 \0 结尾,和文件没有关系。
🍉 文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了 file 结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针 *files, 指向一张表files_struct,该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针!所以本质上,文件描述符就是该数组的下标,只要拿着文件描述符,就可以找到对应的文件
在OS内,系统在访问文件的时候,只认文件描述符fd:
说到了fd,我们就不得不来区分下 FILE 和 fd
FILE 是C库当中提供的一个结构体,而fd 是系统调用,更加接近于底层,因此 FILE 中必定封装了 fd 我们可以来看看 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;//fd的封装
可以看到 int_fileno 就是对 fd 的封装,在这一部分的开头有一大段跟缓冲区相关的内容,为什么要诺列出它呢,我们来看个例子
int main()
{
printf("stdin: %d\n", stdin->_fileno);
printf("stdout: %d\n", stdout->_fileno);
printf("stderr: %d\n", stderr->_fileno);
// stdin、stdout、stderr、file* 必须用到文件描述符
FILE* fp = fopen("log.txt", "w");
printf("fp: %d\n", fp->_fileno);
return 0;
}
补充:fileno 的了解
学了系统调用,我们可以用系统调用接口,也可以用语言提供的文件方法。但还是推荐使用语言提供的方法。因为系统不同,系统调用的接口可能不一样
int main()
{
while(1)
{
printf("%d\n",getpid());
sleep(1);
}
return 0;
}
// 输出:
31926
我们打开另一个终端,查看该进程下的 proc 目录:
进入fd目录,可以看到默认的文件描述符0、1、2是打开的。
打开的设备是dev目录下的pts/3,演示如下:
云服务器下, 我们看到的显示器文件一般在 /dev/pts/目录下,即就是我们打开的终端数
read 函数
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
stat 函数
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *file_name, struct stat *buf);
函数说明: 通过文件名filename获取文件信息,并保存在buf所指的结构体stat中 返回值: 执行成功则返回0,失败返回-1,错误代码存于errno
错误代码:
struct stat {
dev_t st_dev; //文件的设备编号
ino_t st_ino; //节点
mode_t st_mode; //文件的类型和存取的权限
nlink_t st_nlink; //连到该文件的硬连接数目,刚建立的文件值为1
uid_t st_uid; //用户ID
gid_t st_gid; //组ID
dev_t st_rdev; //(设备类型)若此文件为设备文件,则为其设备编号
off_t st_size; //文件字节数(文件大小)
unsigned long st_blksize; //块大小(文件系统的I/O 缓冲区大小)
unsigned long st_blocks; //块数
time_t st_atime; //最后一次访问时间
time_t st_mtime; //最后一次修改时间
time_t st_ctime; //最后一次改变时间(指属性)
};
使用如下:
int main()
{
struct stat st;
int n = stat("log.txt", &st);
if(n < 0) return 1;
printf("file size: %lu\n", st.st_size);
int fd = open("log.txt", O_RDONLY);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
char* file_buffer = (char*)malloc(st.st_size + 1);
n = read(fd, file_buffer, st.st_size);
if(n > 0)
{
file_buffer[n] = '\0';
printf("%s\n", file_buffer);
}
return 0;
}
运行如下:
还记得 我们上面演示得文件描述符从 3 开始嘛,但是当我们先 close 把文件描述符 0 关掉的时候,又会出现什么情况呢?
int main()
{
close(0);
int fd1 = open("log1.txt", O_WRONLY| O_CREAT | O_APPEND, 0666);
int fd2 = open("log2.txt", O_WRONLY| O_CREAT | O_APPEND, 0666);
int fd3 = open("log3.txt", O_WRONLY| O_CREAT | O_APPEND, 0666);
int fd4 = open("log4.txt", O_WRONLY| O_CREAT | O_APPEND, 0666);
printf("fd1: %d\n", fd1);
printf("fd2: %d\n", fd2);
printf("fd3: %d\n", fd3);
printf("fd4: %d\n", fd4);
close(fd1);
close(fd2);
close(fd3);
close(fd4);
return 0;
}
情况一:把 0 关掉,输出如下:
情况二:把 2 关掉,输出如下:
因此我们可以得到一个结论:
刚刚我们演示的是把 0 和 2 关掉,那我们把 1 关掉会是怎样的呢?
我们却发现连打印结果此时都没有了,原因:
但是我们发现log.txt创建出来了,但里面什么东西也没有
然后我们用 fflush 来更新缓冲区,做出如下修改:
fflush(stdout); // TODO
我们可以发现:本来应该向显示器写入,结果却写到了文件中,我们来看下面的一个图片
💖 log.txt 存在磁盘中,当进程启动打开时,就会被加载到内存中。由于我们先关闭了文件描述符1,所以此时 log.txt 的文件描述符就是1。上层的 printf 和 fprintf 都是向 stdout 打印,而 stdout 的描述符是1,OS只认文件描述符,所以最终就向 log.txt 打印了内容。
这个动作我们就叫作 重定向 🍻
🌈 每个文件对象都有对应的内核文件缓冲区,我们写数据都是从上层通过文件描述符1,写到对应的文件缓冲区,然后OS再把内容刷新到磁盘的文件中。
所以 fflush() 里面是 stdout,这是因为我们是刷新语言级别缓冲区的内容到OS的内核缓冲区中,内核缓冲区的内容由OS进行刷新。
💦 因此由上面可知,最开始没有的 fflush 的时候,log.txt 文件里面啥也没有,是因为内容在语言级别的缓冲区中,还没执行到 return 语句,冲刷内容到内核缓冲区中,log.txt 就被关闭了。
🔥 对重定向更深理解,打个比方:
🌈 假如最开始的时候 1 号文件的内容指向显示器,3 号文件内容指向 log.txt。重定向的本质是将 3 号的内容拷贝给 1 号。所以 1 号就不会再指向显示器了,而是变成指向 log.txt,所以后来往 1 号里写的内容都会变成往 log.txt 里写。
💢 struct file 里还存在一个引用计数,有几个指针指向就是几。如 log.txt 由1号和 3 号指向就是2,显示器就是 0
注意:
如下代码:
int main()
{
close(1);
int fd1 = open("log1.txt", O_WRONLY| O_CREAT | O_APPEND, 0666);
printf("printf, fd1: %d\n", fd1);
fprintf(stdout, "fprintf fd: %d\n", fd1);
//fflush(stdout); // TODO
//close(fd1);
return 0;
}
当我们把close也注释掉之后, log1.txt 中也会有内容,如下:
原因:return的时候,语言级别缓冲区的内容就被冲刷到内核文件缓冲区中,此时 log1.txt 也有内容了
结论:当一个进程在退出的时候,会自动刷新自己的缓冲区(所有的FILE对象内部,包括 stdin、stdout、stderr),fclose() -> c 语言 -> 关闭 FILE 的时候,也会自动刷新
因此我们之前 close 没有注释的时候,信息出不来是因为:
刚刚我们演示的操作是最朴素的,是关闭再打开只有一次操作,那么有没有更直接的操作来演示输出重定向操作的呢?
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
代码如下:
int main()
{
int fd = open("log.txt", O_WRONLY| O_CREAT | O_TRUNC, 0666);
dup2(fd, 1);
printf("hello fd: %d\n", fd);
fprintf(stdout, "hello fd: %d\n", fd);
fputs("hello world\n", stdout);
const char*message = "hello fwrite\n";
fwrite(message, 1, strlen(message), stdout);
return 0;
}
把文件打开的方式改成 O_APPEND即可。
因此我们可以知道:输出重定向和追加重定向没有区别,只是打开的方式不一样而已。
先实现一段从标准输入读的代码
int main()
{
char buffer[2048];
size_t s = read(0, buffer, sizeof(buffer));
if(s > 0)
{
buffer[s - 1] = 0;
printf("stdin redir: \n%s\n", buffer);
}
return 0;
}
运行如下:
我们加上输入重定向的代码到 main 函数的最上面几行
// 输入重定向:需要文件存在且可读
int fd = open("log.txt", O_RDONLY);
dup2(fd, 0);
🔥 原因:拿 fd 新打开文件输入地址来覆盖 0 ,覆盖 0 之后,由于可是当前已经执行 log.txt 文件了,所以最后读数据会去 log.txt 去读,这就是 输入重定向
🍒 因此我们得到一个结论:其实就是新打开一个文件,然后把 流 做一下 dup2 重定向,在内核当中做一个文件内容的拷贝,拷贝后续代码不变,就会自动更改读取数据的数据源,这也就是 重定向
缓冲区存在的意义:OS为语言考虑,语言为用户考虑。给上层提供高效的IO体验,间接提高整体效率。
这个刷新策略在内核和用户级别的缓冲区都能用。这里介绍用户级别的
int main()
{
// C 库函数
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
const char* message = "hello fwrite\n";
fwrite(message, 1, strlen(message), stdout);
// 系统调用
const char *w = "hello write\n";
write(1, w, strlen(w));
return 0;
}
运行上面代码,第一次在显示器上打印,第二次重定向到文件打印。发现打印的顺序不同
原因如下:
将上面代码改成如下,让其产生子进程
直接运行的结果跟上面的一样,但是当输入到文件中,结果就与上面不一样了,如下:
原因如下:
解释:
那为啥执行结果却只有四行呢?
因为直接打印的时候是向显示器文件打,显示器文件打的是行刷新,刷新出 \n 的内容,此时再进行 fork ,当前缓冲区的内容已经被刷新完了,没有刷的了
比如当我们去掉换行符,没有行刷新输出如下:
int main()
{
printf("hello printf ");
fprintf(stdout, "hello fprintf ");
const char* message = "hello fwrite ";
fwrite(message, 1, strlen(message), stdout);
// 系统调用
const char *w = "hello write\n ";
write(1, w, strlen(w));
// 创建子进程
fork();
return 0;
}
🍉 看了这么多,那我们可不可以 命令行参数,重定向方式用 符号表示,然后在程序中做判断用哪个重定向,然后再把文件以特定形式打开
[lighthouse@VM-8-10-centos File-IO]$ ./filecode > log.txt
如下:
// 全局遍历 与 重定向有关
#define NoneRedir 0
#define InputRedir 1
#define OutputRedir 2
#define AppRedir 3
int redir = NoneRedir;
char *filename = nullptr;
// " "file.txt 从左向右扫描不是空格的字符
// do while(0) 套壳
#define TrimSpace(pos) do{\
while(isspace(*pos)){\
pos++;\
}\
}while(0)
上面为啥会用 do while(0) 来封装 宏
目的是为了解决宏定义在使用时可能引发的一些问题,例如宏定义中的分号和大括号的使用。 }while (0),将你的代码写在里面,里面可以定义变量而不用考虑变量名会同函数之前或者之后的重复。 ,允许在宏定义中使用局部变量。 总而言之, do {} while (0) 的作用是为了解决宏定义在使用时可能引发的一些问题,确保宏定义可以作为单个语句使用,并且在逻辑上看起来像是一个语句。
函数实现如下:
void ParseCommandLine(char command_buffer[], int len) // 3. 分析命令
{
(void)len; // 避免不使用的时候告警
// 虽然定义的是全局默认为0,但是由于这些工作都是重复去做的,为保证安全性,需要局部初始为0
memset(gargv, 0, sizeof(gargv));
gargc = 0;
// 重定向
redir = NoneRedir;
filename = nullptr;
printf("command start: %s\n", command_buffer);
// "ls -a -b -c -d " > hello.txt
// "ls -a -b -c -d " >> hello.txt
// "ls -a -b -c -d " < hello.txt
int end = len - 1;
while(end >= 0)
{
if(command_buffer[end] == '<') // 输入重定向
{
redir = InputRedir;
// 拿到干净的文件名
command_buffer[end] = 0;
filename = &command_buffer[end] + 1;
TrimSpace(filename); // 跳过空格部分
break;
}
else if(command_buffer[end] == '>')
{
if(command_buffer[end - 1] == '>') // 追加重定向
{
redir = AppRedir;
command_buffer[end] = 0;
command_buffer[end - 1] = 0;
filename = &command_buffer[end] + 1;
TrimSpace(filename);
break;
}
else // 输出重定向
{
redir = OutputRedir;
command_buffer[end] = 0;
filename = &command_buffer[end] + 1;
TrimSpace(filename);
break;
}
}
else
{
end--;
}
}
// 拆分读取的字符串
// "ls -a -l -n"
const char *sep = " "; //分隔符
for(char* ch = strtok(command_buffer, sep); (bool) ch; ch = strtok(nullptr, sep))
{
gargv[gargc++] = ch;
}
}
....
// 测试代码如下:
int main()
{
InitEnv(); // 初始化环境变量表
char command_buffer[basesize];
while(true) // 不断重复该工作
{
PrintCommandLine(); // 1. 命令行提示符
// command_buffer -> output(输出型参数),把 ls -a -l 看作一个字符串
if(!GetCommandLine(command_buffer, basesize)) // 2. 获取用户命令
{
continue;
}
//printf("%s\n", command_buffer); //测试
// ls -a -b -c 解析每个指令 > "ls" "-a" "-b" "-c" 拆成一个一个字符串
// 重定向格式
ParseCommandLine(command_buffer, strlen(command_buffer)); // 3. 分析命令
// 检测
printf("redir: %d\n", redir);
printf("filename: %s\n", filename);
printf("command end: %s\n", command_buffer);
if(CheckAndExecBuiltCommand())
{
continue;
}
ExecuteCommand(); // 4. 执行命令
}
return 0;
}
输出如下:
现在我们就完成了基本的分析,接下来就可以在后续执行代码之前来进行我们的重定向,需要实现对子进程的重定向(因为命令是由子进程来做),需要解决程序替换对重定向的影响
// 在 shell 中
// 有些命令,必须由子进程来执行
// 有些命令,不能由子进程来执行,由shell 自己执行 --- 内建命令
bool ExecuteCommand() // 4. 执行命令
{
// 让子进程进行执行
pid_t id = fork();
if(id < 0) return false;
if(id == 0)
{
// 1. 重定向应该让子进程自己做
// 2. 程序替换会不会影响重定向
if(redir == InputRedir) // 输入重定向
{
if(filename)
{
int fd = open(filename, O_RDONLY);
if(fd < 0) // 子进程打开失败
{
exit(2);
}
dup2(fd, 0);
}
else
{
exit(1);
}
}
else if(redir == OutputRedir) // 输出重定向
{
if(filename)
{
int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
if(fd < 0) // 子进程打开失败
{
exit(4);
}
dup2(fd, 1);
}
else
{
exit(3);
}
}
else if(redir == AppRedir) // 追加重定向
{
if(filename)
{
int fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666);
if(fd < 0) // 子进程打开失败
{
exit(6);
}
dup2(fd, 1);
}
else
{
exit(5);
}
}
else
{
// 没有重定向,Do Nothing
}
// 子进程
// 1. 执行命令
//execvp(gargv[0], gargv);
execvpe(gargv[0], gargv, genv); // 把我们的环境变量传递给子进程了
// 2. 退出
exit(1); // 要进行程序替换,只有子进程失败,才会 exit。
}
int status = 0;
pid_t rid = waitpid(id, &status, 0); // 阻塞等待
if(rid > 0)
{
if(WIFEXITED(status)) // 等待成功获取退出信息
{
lastcode = WEXITSTATUS(status);
}
else
{
lastcode = 100; //非正常退出
}
return true;
}
return false;
}
运行结果如下:
mystdio.h 封装
#pragma once
#define SIZE 1024
#define FLUSH_NONE 0
#define FLUSH_LINE 1
#define FLUSH_FULL 2
struct IO_FILE
{
int flag; // 刷新方式
int fileno; // 文件描述符
char outbuffer[SIZE];
// 缓冲区
int cap; // 容量
int size; // 大小
// TODO
};
typedef struct IO_FILE mFILE;
mFILE *mfopen(const char *filename, const char*mode); //打开文件的操作
int mfwrite(const void *ptr, int num, mFILE *stream);
void mfflush(mFILE *stream);
void mfclose(mFILE *stream);
先实现第一个操作,如下:
#include "my_stdio.h"
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
mFILE *mfopen(const char *filename, const char*mode)
{
int fd = -1;
if(strcmp(mode, "r") == 0)
{
fd = open(filename, O_RDONLY);
}
else if(strcmp(mode, "w") == 0)
{
fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
}
else if(strcmp(mode, "a") == 0)
{
fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666);
}
if(fd < 0) return NULL;
mFILE *mf = (mFILE*)malloc (sizeof(mFILE));
if(!mf)
{
close(fd);
return NULL;
}
mf->fileno = fd;
mf->flag = FLUSH_LINE;
mf->size = 0;
mf->cap = SIZE;
return mf;
}
然后编译生成 .o 文件,如下:
[lighthouse@VM-8-10-centos stdio]$ gcc -c my_stdio.c
🌈 然后我们再新建一个文件夹,将我们的头文件和之前生成 .o 文件移到当前目录下,然后在当前目录下新建 main.c 文件,用给定的东西来进行以下操作:
#include "my_stdio.h"
#include <stdio.h>
int main()
{
mFILE *fp = mfopen("./log.txt", "w");
printf("%d, %d, %d, %d\n", fp->fileno, fp->flag, fp->cap, fp->size);
return 0;
}
🔥 然后生成 .o 文件,然后将两个 .o 文件链接生成可执行程序:
🍉 运行可执行程序,结果如下:
完整my_stduo,c函数实现
#include "my_stdio.h"
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
mFILE *mfopen(const char *filename, const char*mode)
{
int fd = -1;
if(strcmp(mode, "r") == 0)
{
fd = open(filename, O_RDONLY);
}
else if(strcmp(mode, "w") == 0)
{
fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
}
else if(strcmp(mode, "a") == 0)
{
fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666);
}
if(fd < 0) return NULL;
mFILE *mf = (mFILE*)malloc (sizeof(mFILE));
if(!mf)
{
close(fd);
return NULL;
}
mf->fileno = fd;
mf->flag = FLUSH_LINE;
mf->size = 0;
mf->cap = SIZE;
return mf;
}
void mfflush(mFILE *stream)
{
if(stream->size > 0) // 缓冲区里面有内容
{
write(stream->fileno, stream->outbuffer, stream->size);
// 刷新到外设
stream->size = 0;
}
}
int mfwrite(const void *ptr, int num, mFILE *stream)
{
// 1. 拷贝
memcpy(stream->outbuffer + stream->size, ptr, num);
stream->size += num;
// 2. 检测缓冲区是否要刷新
if(stream->flag == FLUSH_LINE && stream->size > 0 && stream->outbuffer[stream->size - 1] == '\n')
{
mfflush(stream);
}
return num;
}
void mfclose(mFILE *stream)
{
// 刷新之前,先判断缓冲区是否有内容
// 将内容刷新到缓冲区
if(stream->size > 0)
{
mfflush(stream);
}
close(stream->fileno); // 进行文件刷新
}
测试:
#include "my_stdio.h"
#include <string.h>
#include <stdio.h>
#include <unistd.h>
int main()
{
mFILE *fp = mfopen("./log.txt", "a");
if(fp == NULL)
{
return 1;
}
int cnt = 3;
while(cnt)
{
printf("write: %d\n", cnt);
char buffer[64];
snprintf(buffer, sizeof(buffer), "hello message, number is : %d\n", cnt);
cnt--;
mfwrite(buffer, strlen(buffer), fp);
sleep(1);
}
mfclose(fp);
}
当我们把 snprintf 中 的 \n 去掉,加上一行代码,我们可以看看下面的过程:
从上面我们可以知道:说明我们在多次写入时,没有写到内核级缓冲区,而是写到了 my_file 结构当中,但是我们可以自己用fflush()强制刷新,
我们也可以把 fsnyc 写到我们实现 my_stdio.c 文件的 mfflush 中来实现这个效果
void mfflush(mFILE *stream)
{
if(stream->size > 0) // 缓冲区里面有内容
{
// 写到内核文件的文件缓冲区中!!
write(stream->fileno, stream->outbuffer, stream->size);
// 刷新到外设
fsync(stream->fileno);
stream->size = 0;
}
}
我们这篇博客主要讲了关于 文件描述符 和 缓冲区的概念,大家可以多多理解,方便我们后面的学习
【*★,°*:.☆( ̄▽ ̄)/$:*.°★* 】那么本篇到此就结束啦,如果我的这篇博客可以给你提供有益的参考和启示,可以三连支持一下 !