目录
1. 理解"⽂件"
1-1 狭义理解
1-2 ⼴义理解
1-3 ⽂件操作的归类认知
1-4 系统⻆度
访问文件,需要先打开文件!那么是由谁打开文件???
操作系统要不要把被打开的文件管理起来?
2. 回顾C⽂件接⼝
2.1 fopen("文件名","打开方式");
snprintf()
打开的myfile⽂件在哪个路径下?
2-2 hello.c写⽂件
2-3 hello.c读⽂件
稍作修改,实现简单cat命令:
2-4 输出信息到显⽰器,你有哪些⽅法
2-5 stdin & stdout & stderr
系统调用接口:
open:
对于系统写入write:
结论:
3.系统读文件:
3.1.open返回值:
那么为什么语言层要进行各自的封装呢?
语言为什么要增加自己的可移植性???
文件描述符是什么???
3-2 ⽂件描述符fd
文件描述符的创建:
重定向:
3.3 在minishell中添加重定向功能(上一章节的后续添加重定向功能)
4.理解一切皆文件
这里就体现了多态的思想:
5.缓冲区
5-1 什么是缓冲区
5-2 为什么要引⼊缓冲区机制
5-3 缓冲类型
5-4 FILE
5-5 简单设计⼀下libc库:
my_stdio.h
my_stdio.c
main.c
总结不易~本章节对我有很大的收获,希望对你也是!!!
1. 理解"⽂件"
1-1 狭义理解
• ⽂件在磁盘⾥• 磁盘是永久性存储介质,因此⽂件在磁盘上的存储是永久性的• 磁盘是外设(即是输出设备也是输⼊设备)• 磁盘上的⽂件 本质是对⽂件的所有操作,都是对外设的输⼊和输出 简称 IO
1-2 ⼴义理解
• Linux 下⼀切皆⽂件(键盘、显⽰器、⽹卡、磁盘…… 这些都是抽象化的过程)(后⾯会讲如何去理解)
1-3 ⽂件操作的归类认知
• 对于 0KB 的空⽂件是占⽤磁盘空间的• ⽂件是⽂件属性(元数据)和⽂件内容的集合(⽂件 = 属性(元数据)+ 内容)• 所有的⽂件操作本质是⽂件内容操作和⽂件属性操作
1-4 系统⻆度
访问文件,需要先打开文件!那么是由谁打开文件???
是由进程来打开文件!对文件操作,本质是:进程对文件的操作!
操作系统要不要把被打开的文件管理起来?
要!就是先描述,在组织!!!
2. 回顾C⽂件接⼝
2.1 fopen("文件名","打开方式");
虽然这里只传入了一个文件名,但是该调用会将当前路径pwd拼接上当前文件名来进行寻找并打开,在之前的创建文件也是同样如此,获取当前位置的pwd后来创建当前路径的文件!
来认识一下snprintf()这个安全的字符串格式化函数,常用于格式化并将结果写入字符数组中。它可以防止因数组边界溢出导致的安全问题。
int snprintf(char *str, size_t size, const char *format, ...);
str
:目标缓冲区,用于存储格式化后的字符串。size
:缓冲区的大小(包括结尾的空字符\0
)。如果size
为 0,则snprintf()
不会向目标缓冲区写入任何字符。format
:格式字符串,与printf
的格式类似,支持%d
、%s
等格式说明符。...
:可变参数,提供用于格式化的值。
#include <stdio.h>int main()
{int cnt = 1;const char* msg = "hello,bit: ";while (cnt < 10) {char buffer[1024];snprintf(buffer, sizeof(buffer), "%s%d",msg, cnt++);printf("%s\n", buffer); // 输出到屏幕}return 0;
}
snprintf()
1.每次都是写入这个局部变量buffer内;2.然后计算输入的大小,保证不会发生越界;3.输入的内容“字符串”;4.可变参数列表
输出为:
hello,bit: 1
hello,bit: 2
hello,bit: 3
hello,bit: 4
hello,bit: 5
hello,bit: 6
hello,bit: 7
hello,bit: 8
hello,bit: 9
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
ptr
:指向要写入数据的内存地址。size
:每个数据单元的大小(以字节为单位)。count
:写入的数据单元数量。stream
:目标文件的指针(FILE *
类型,通常通过fopen
获得)。
fwrite(buffer,strlen(buffer),1,fp);
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
ptr
:目标缓冲区的指针,用于存储读取到的数据。size
:每个数据单元的大小(以字节为单位)。count
:要读取的数据单元数量。stream
:目标文件指针(FILE *
类型,通常通过fopen
打开文件获得)。
- 返回成功读取的 数据单元数量(
count
)。- 如果返回值小于请求的
count
,可能是因为:
- 文件结束(EOF)。
- 发生错误。
- 可以通过
feof()
和ferror()
来检查文件状态。
feof()函数可以判断是否读取到文件的末尾
#include <stdio.h>
int main()
{FILE *fp = fopen("myfile", "w");if(!fp){printf("fopen error!\n");
}while(1);fclose(fp);return 0;
}
打开的myfile⽂件在哪个路径下?
• 在程序的当前路径下,那系统怎么知道程序的当前路径在哪⾥呢?
[hyb@VM-8-12-centos io]$ ps ajx | grep myProc
506729 533463 533463 506729 pts/249 533463 R+ 1002 7:45 ./myProc
536281 536542 536541 536281 pts/250 536541 R+ 1002 0:00 grep --
color=auto myProc
[hyb@VM-8-12-centos io]$ ls /proc/533463 -l
total 0
......
-r--r--r-- 1 hyb hyb 0 Aug 26 17:01 cpuset
lrwxrwxrwx 1 hyb hyb 0 Aug 26 16:53 cwd -> /home/hyb/io
-r-------- 1 hyb hyb 0 Aug 26 17:01 environ
lrwxrwxrwx 1 hyb hyb 0 Aug 26 16:53 exe -> /home/hyb/io/myProc
dr-x------ 2 hyb hyb 0 Aug 26 16:54 fd
......
• cwd:指向当前进程运⾏⽬录的⼀个符号链接。• exe:指向启动当前进程的可执⾏⽂件(完整路径)的符号链接。打开⽂件,本质是进程打开,所以,进程知道⾃⼰在哪⾥,即便⽂件不带路径,进程也知道。由此OS就能知道要创建的⽂件放在哪⾥。
2-2 hello.c写⽂件
#include <stdio.h>
#include <string.h>
int main()
{FILE* fp = fopen("myfile", "w");if (!fp) {printf("fopen error!\n");}const char* msg = "hello bit!\n";int count = 5;while (count--) {fwrite(msg, strlen(msg), 1, fp);}fclose(fp);return 0;
}
2-3 hello.c读⽂件
#include <stdio.h>
#include <string.h>
int main()
{FILE* fp = fopen("myfile", "r");if (!fp) {printf("fopen error!\n");return 1;}char buf[1024];const char* msg = "hello bit!\n";while (1) {//注意返回值和参数,此处有坑,仔细查看man⼿册关于该函数的说明size_t s = fread(buf, 1, strlen(msg), fp);if (s > 0) {buf[s] = 0;printf("%s", buf);}if (feof(fp)) {break;}}fclose(fp);return 0;
}
稍作修改,实现简单cat命令:
#include <stdio.h>
#include <string.h>//cat myfile
int main(int argc, char* argv[])
{if (argc != 2){printf("Usage: %s filename\n", argv[0]);return 1;}FLIE* fp = fopen(argv[1], "r")if (fp == NULL){perror("fopen");return 2;}while (1){char buffer[128];memset(buffer, 0, sizeof(buffer));int n = fread(buffer, sizeof(buffer) - 1, 1, fp);if (n > 0) printf("%s", buffer);if (feof(fp)) break;}fclose(fp);return 0;
}
2-4 输出信息到显⽰器,你有哪些⽅法
#include <stdio.h>
#include <string.h>
int main()
{const char* msg = "hello fwrite\n";fwrite(msg, strlen(msg), 1, stdout);printf("hello printf\n");fprintf(stdout, "hello fprintf\n");return 0;
}
2-5 stdin & stdout & stderr
#include <stdio.h>
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;
系统调用接口:
O_CREAT:创建O_WRONLY:只写
O_TRUNC:清空
open:
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
int fd = open("log.txt",O_CREAT | O_WRONLY | O_TRUNC,0666);
所以由上面看到,想要追加就不能清空:O_APPEND
对于系统写入write:
结论:
再系统层面上,并不关心你是二进制写入还是字符写入,最终系统都会转换成二进制来进行识别
对于语言层进行封装的二进制 或 字符写入的接口都是调用的系统write!
3.系统读文件:
那么就不能再打开的时候进行新建,因为当前文件不存在还要新建再打开读是没有意义的!所以读文件不需要新建文件,也不需要清空文件,不需要写入文件,只需要转换为二进制 O_RDONLY,只读方式打开文件
int fd = open("log.txt",O_RDONLY);
这也就是为什么write存在两参数调用的接口,就是为读来准备的
int n = read(fd,buffer,sizeof(buffer)-1);
3.1.open返回值:
那么为什么语言层要进行各自的封装呢?
就是因为每个OS的实现不同,就是我们上面写的代码放在windows下就跑步过去,因为OS不同,底层接口实现的就不一样,如果我们实现一种语言来将各个OS进行封装,我们写一套C语言代码,就可以实现跨平台移植的作用,在各个OS上只需呀裁掉别的平台的所有代码,只保留当前OS的代码,就是当前OS的接口封装即可,这样就凸显出了语言的可移植性!
语言为什么要增加自己的可移植性???
就是为了能够满足各个平台的人,让更多人去使用,占有市场利用率,防止被淘汰
文件描述符是什么???
3-2 ⽂件描述符fd
• 通过对open函数的学习,我们知道了⽂件描述符就是⼀个⼩整数
文件描述符的创建:
FILE是由一个结构体来进行封装的,每一个文件的属性都被封装到一个struct_file内,多个struct_file由一个双链表进行链接,这样先描述,在组织;本质上也就是对一个链表进行增删查改
对于整个链表的管理,还设置了一个文件描述符表 struct file *fd array[] 指针数组,来将整个文件链表的每一个节点存入这个指针数组内,也就有了为什么可以打印出fd的值的下标,这个指针数组再由一个struct files_struct *files指针进行指向
重定向:
首先进入程序close(1)关掉了stdout标准输出,然后此时的系统就将fd分配给1位置进行指向,然后printf()底层封装的就是原来指向stdout的1,现在又重新指向log.txt位置进行打印,就往文本log.txt内进行打印得到该现象!
fgets()
函数用于从指定的流(在这个例子中是 stdin
,标准输入流)中读取一行字符,并将其存储到 buffer
中。成功了就打印buffer~
设置任意文本的输出重定向:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main() {int fd = open("./log", O_CREAT | O_RDWR);if (fd < 0) {perror("open");return 1;}close(1);dup2(fd, 1);for (;;) {char buf[1024] = { 0 };ssize_t read_size = read(0, buf, sizeof(buf) - 1);if (read_size < 0) {perror("read");break;}printf("%s", buf);fflush(stdout);}return 0;
}
printf是C库当中的IO函数,⼀般往 stdout 中输出,但是stdout底层访问⽂件的时候,找的还是fd:1, 但此时,fd:1下标所表⽰内容,已经变成了myfifile的地址,不再是显⽰器⽂件的地址,所以,输出的任何消息都会往⽂件中写⼊,进⽽完成输出重定向。那追加和输⼊重定向如何完成呢?
3.3 在minishell中添加重定向功能(上一章节的后续添加重定向功能)
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <ctype.h>
using namespace std;
const int basesize = 1024;
const int argvnum = 64;
const int envnum = 64;
// 全局的命令⾏参数表
char* gargv[argvnum];
int gargc = 0;
// 全局的变量
int lastcode = 0;
// 我的系统的环境变量
char* genv[envnum];
// 全局的当前shell⼯作路径
char pwd[basesize];
char pwdenv[basesize];
// 全局变量与重定向有关
#define NoneRedir 0
#define InputRedir 1
#define OutputRedir 2
#define AppRedir 3
int redir = NoneRedir;
char* filename = nullptr;
// " "file.txt
#define TrimSpace(pos) do{\
while(isspace(*pos)){\
pos++;\
}\
}while(0)
string GetUserName()
{string name = getenv("USER");return name.empty() ? "None" : name;
}
string GetHostName()
{string hostname = getenv("HOSTNAME");return hostname.empty() ? "None" : hostname;
}
string GetPwd()
{if (nullptr == getcwd(pwd, sizeof(pwd))) return "None";snprintf(pwdenv, sizeof(pwdenv), "PWD=%s", pwd);putenv(pwdenv); // PWD=XXXreturn pwd;//string pwd = getenv("PWD");//return pwd.empty() ? "None" : pwd;
}
string LastDir()
{string curr = GetPwd();if (curr == "/" || curr == "None") return curr;// /home/whb/XXXsize_t pos = curr.rfind("/");if (pos == std::string::npos) return curr;return curr.substr(pos + 1);
}
string MakeCommandLine()
{// [whb@bite-alicloud myshell]$char command_line[basesize];snprintf(command_line, basesize, "[%s@%s %s]# ", \GetUserName().c_str(), GetHostName().c_str(), LastDir().c_str());return command_line;
}
void PrintCommandLine() // 1. 命令⾏提⽰符
{printf("%s", MakeCommandLine().c_str());fflush(stdout);
}
bool GetCommandLine(char command_buffer[], int size) // 2. 获取⽤⼾命令
{// 我们认为:我们要将⽤⼾输⼊的命令⾏,当做⼀个完整的字符串// "ls -a -l -n"char* result = fgets(command_buffer, size, stdin);if (!result){return false;}command_buffer[strlen(command_buffer) - 1] = 0;if (strlen(command_buffer) == 0) return false;return true;
}
void ResetCommandline()
{memset(gargv, 0, sizeof(gargv));gargc = 0;// 重定向redir = NoneRedir;filename = nullptr;
}
void ParseRedir(char command_buffer[], int len)
{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--;}}
}
void ParseCommand(char command_buffer[])
{// "ls -a -l -n"const char* sep = " ";gargv[gargc++] = strtok(command_buffer, sep);// =是刻意写的while ((bool)(gargv[gargc++] = strtok(nullptr, sep)));gargc--;
}
void ParseCommandLine(char command_buffer[], int len) // 3. 分析命令
{ResetCommandline();ParseRedir(command_buffer, len);ParseCommand(command_buffer);//printf("command start: %s\n", command_buffer);// "ls -a -l -n"// "ls -a -l -n" > file.txt// "ls -a -l -n" < file.txt// "ls -a -l -n" >> file.txt//printf("redir: %d\n", redir);//printf("filename: %s\n", filename);//printf("command end: %s\n", command_buffer);
}
void debug()
{printf("argc: %d\n", gargc);for (int i = 0; gargv[i]; i++){printf("argv[%d]: %s\n", i, gargv[i]);}
}
//enum
//{
// FILE_NOT_EXISTS = 1,
// OPEN_FILE_ERROR,
//};
void DoRedir()
{// 1. 重定向应该让⼦进程⾃⼰做!// 2. 程序替换会不会影响重定向?不会// 0. 先判断 && 重定向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 Nothong!}
}
// 在shell中
// 有些命令,必须由⼦进程来执⾏
// 有些命令,不能由⼦进程执⾏,要由shell⾃⼰执⾏ --- 内建命令 built command
bool ExecuteCommand() // 4. 执⾏命令
{// 让⼦进程进⾏执⾏pid_t id = fork();if (id < 0) return false;if (id == 0){//⼦进程DoRedir();// 1. 执⾏命令execvpe(gargv[0], gargv, genv);// 2. 退出exit(7);}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;
}
void AddEnv(const char* item)
{int index = 0;while (genv[index]){index++;}genv[index] = (char*)malloc(strlen(item) + 1);strncpy(genv[index], item, strlen(item) + 1);genv[++index] = nullptr;
}
// shell⾃⼰执⾏命令,本质是shell调⽤⾃⼰的函数
bool CheckAndExecBuiltCommand()
{if (strcmp(gargv[0], "cd") == 0){// 内建命令if (gargc == 2){chdir(gargv[1]);lastcode = 0;}else{lastcode = 1;}return true;}else if (strcmp(gargv[0], "export") == 0){// export也是内建命令if (gargc == 2){AddEnv(gargv[1]);lastcode = 0;}else{lastcode = 2;}return true;}else if (strcmp(gargv[0], "env") == 0){for (int i = 0; genv[i]; i++){printf("%s\n", genv[i]);}lastcode = 0;return true;}else if (strcmp(gargv[0], "echo") == 0){if (gargc == 2){// echo $?// echo $PATH
// echo helloif (gargv[1][0] == '$'){if (gargv[1][1] == '?'){printf("%d\n", lastcode);lastcode = 0;}}else{printf("%s\n", gargv[1]);lastcode = 0;}}else{lastcode = 3;}return true;}return false;
}
// 作为⼀个shell,获取环境变量应该从系统的配置来
// 我们今天就直接从⽗shell中获取环境变量
void InitEnv()
{extern char** environ;int index = 0;while (environ[index]){genv[index] = (char*)malloc(strlen(environ[index]) + 1);strncpy(genv[index], environ[index], strlen(environ[index]) + 1);index++;}genv[index] = nullptr;
}
int main()
{InitEnv();char command_buffer[basesize];while (true){PrintCommandLine(); // 1. 命令⾏提⽰符// command_buffer -> outputif (!GetCommandLine(command_buffer, basesize)) // 2. 获取⽤⼾命令{continue;}//printf("%s\n", command_buffer);//ls//"ls -a -b -c -d"->"ls" "-a" "-b" "-c" "-d"//"ls -a -b -c -d">hello.txt//"ls -a -b -c -d">>hello.txt//"ls -a -b -c -d"<hello.txtParseCommandLine(command_buffer, strlen(command_buffer)); // 3. 分析命令if (CheckAndExecBuiltCommand()){continue;}ExecuteCommand(); // 4. 执⾏命令}return 0;
}
其实文件描述符1标准输出,2标准错误,都是指向同一个文件,说明只做了标准输入的重定向,并没有做出标准错误的重定向:
4.理解一切皆文件
⾸先,在windows中是⽂件的东西,它们在linux中也是⽂件;其次⼀些在windows中不是⽂件的东西,⽐如进程、磁盘、显⽰器、键盘这样硬件设备也被抽象成了⽂件,你可以使⽤访问⽂件的⽅法访问它们获得信息;甚⾄管道,也是⽂件;将来我们要学习⽹络编程中的socket(套接字)这样的东西, 使⽤的接⼝跟⽂件接⼝也是⼀致的。
这样做最明显的好处是,开发者仅需要使⽤⼀套 API 和开发⼯具,即可调取 Linux 系统中绝⼤部分的资源。举个简单的例⼦,Linux 中⼏乎所有读(读⽂件,读系统状态,读PIPE)的操作都可以⽤read 函数来进⾏;⼏乎所有更改(更改⽂件,更改系统参数,写 PIPE)的操作都可以⽤ write 函数来进⾏。 之前我们讲过,当打开⼀个⽂件时,操作系统为了管理所打开的⽂件,都会为这个⽂件创建⼀个file结构体,该结构体定义在 /usr/src/kernels/3.10.0- 1160.71.1.el7.x86_64/include/linux/ fs.h 下,以下展⽰了该结构部分我们关系的内容:
值得关注的是 struct file 中的 f_op 指针指向了⼀个 file_operations 结构体,这个结构体中的成员除了struct module* owner 其余都是函数指针。该结构和 struct file 都在fs.h下。
这里就体现了多态的思想:
- 是面向对象编程中的一个重要特性,它允许使用统一的接口来表示不同的行为。在 C++ 中,多态主要通过虚函数(Virtual Function)来实现。
- 简单来说,多态就是 “多种形态”。例如,对于不同类型的动物(如猫、狗),它们都有 “叫” 这个行为,但叫声不同。多态可以让我们通过一个共同的 “叫” 函数接口,来实现不同动物的不同叫声。
file_operation 就是把系统调⽤和驱动程序关联起来的关键数据结构,这个结构的每⼀个成员都对应着⼀个系统调⽤。读取 file_operation 中相应的函数指针,接着把控制权转交给函数,从⽽完成了Linux设备驱动程序的⼯作。
上图中的外设,每个设备都可以有⾃⼰的read、write,但⼀定是对应着不同的操作⽅法!!但通过struct file 下 file_operation 中的各种函数回调,让我们开发者只⽤file便可调取 Linux 系统中绝⼤部分的资源!!这便是“linux下⼀切皆⽂件”的核⼼理解
5.缓冲区
5-1 什么是缓冲区
缓冲区是内存空间的⼀部分。也就是说,在内存空间中预留了⼀定的存储空间,这些存储空间⽤来缓冲输⼊或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输⼊设备还是输出设备,分为输⼊缓冲区和输出缓冲区。
5-2 为什么要引⼊缓冲区机制
读写⽂件时,如果不会开辟对⽂件操作的缓冲区,直接通过系统调⽤对磁盘进⾏操作(读、写等),那么每次对⽂件进⾏⼀次读写操作时,都需要使⽤读写系统调⽤来处理此操作,即需要执⾏⼀次系统调⽤,执⾏⼀次系统调⽤将涉及到CPU状态的切换,即从⽤⼾空间切换到内核空间,实现进程上下⽂的切换,这将损耗⼀定的CPU时间,频繁的磁盘访问对程序的执⾏效率造成很⼤的影响。
为了减少使⽤系统调⽤的次数,提⾼效率,我们就可以采⽤缓冲机制。⽐如我们从磁盘⾥取信息,可以在磁盘⽂件进⾏操作时,可以⼀次从⽂件中读出⼤量的数据到缓冲区中,以后对这部分的访问就不需要再使⽤系统调⽤了,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作⼤ 快于对磁盘的操作,故应⽤缓冲区可⼤ 提⾼计算机的运⾏速度。⼜⽐如,我们使⽤打印机打印⽂档,由于打印机的打印速度相对较慢,我们先把⽂档输出到打印机相应的缓冲区,打印机再⾃⾏逐步打印,这时我们的CPU可以处理别的事情。可以看出,缓冲区就是⼀块内存区,它⽤在输⼊输出设备和CPU之间,⽤来缓存数据。它使得低速的输⼊输出设备和⾼速的CPU能够协调⼯作,避免低速的输⼊输出设备占⽤CPU,解放出CPU,使其能够⾼效率⼯作。
5-3 缓冲类型
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {close(1);int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);if (fd < 0) {perror("open");return 0;}printf("hello world: %d\n", fd);close(fd);return 0;
}
[hyb@VM-8-12-centos buffer]$ ./myfile
[hyb@VM-8-12-centos buffer]$ ls
log.txt makefile myfile myfile.c
[hyb@VM-8-12-centos buffer]$ cat log.txt
[hyb@VM-8-12-centos buffer]$
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
perror("open");
return 0;
}
printf("hello world: %d\n", fd);
fflush(stdout);
close(fd);
return 0;
}
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {close(2);int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);if (fd < 0) {perror("open");return 0;}perror("hello world");close(fd);return 0;
}
[hyb@VM-8-12-centos buffer]$ ./myfile
[hyb@VM-8-12-centos buffer]$ cat log.txt
hello world: Success
5-4 FILE
#include <stdio.h>
#include <string.h>
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;
}
hello printfhello fwritehello write
hello writehello printfhello fwritehello printfhello fwrite
• ⼀般C库函数写⼊⽂件时是全缓冲的,⽽写⼊显⽰器是⾏缓冲。• printf fwrite 库函数+会⾃带缓冲区(进度条例⼦就可以说明),当发⽣重定向到普通⽂件时,数据的缓冲⽅式由⾏缓冲变成了全缓冲。• ⽽我们放在缓冲区中的数据,就不会被⽴即刷新,甚⾄fork之后• 但是进程退出之后,会统⼀刷新,写⼊⽂件当中。• 但是fork的时候,⽗⼦数据会发⽣写时拷⻉,所以当你⽗进程准备刷新的时候,⼦进程也就有了同样的⼀份数据,随即产⽣两份数据。• write 没有变化,说明没有所谓的缓冲。
typedef struct _IO_FILE FILE; 在 /usr/include/stdio.h
5-5 简单设计⼀下libc库:
my_stdio.h
$ cat my_stdio.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);
my_stdio.c
$ cat my_stdio.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);// 刷新到外设fsync(stream->fileno);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);
}
main.c
$ cat main.c
#include "my_stdio.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{mFILE* fp = mfopen("./log.txt", "a");if (fp == NULL){return 1;}int cnt = 10;while (cnt){printf("write %d\n", cnt);char buffer[64];snprintf(buffer, sizeof(buffer), "hello message, number is : %d", cnt);cnt--;mfwrite(buffer, strlen(buffer), fp);mfflush(fp);sleep(1);}mfclose(fp);
}