Linux——基础IO(1)

 

目录

0. 文件先前理解

1. C文件接口

1.1 写文件

1.2 读文件

1.3 输出信息到显示器

1.4 总结 and stdin & stdout & stderr

2. 系统调用文件I/O

2.1 系统接口使用示例

2.2 接口介绍

2.3 open函数返回值

3. 文件描述符fd及重定向

3.1 0 & 1 & 2

3.2 文件描述符fd的理解

3.3 文件描述符Linux内核源码分析

3.4 文件描述符的分配规则

3.5 重定向

3.6 使用 dup2 系统调用

4. 在minishell中添加重定向功能

5. 有关FILE缓冲区及Linux一切皆文件理解

5.1 一切皆文件及C语言实现运行时多态

5.2 缓冲区

5.3 C标准库FILE结构体及缓冲区

5.4 大致模拟缓冲区设计


0. 文件先前理解

文件 = 文件内容 + 文件属性

因此,对文件进行的操作无非:对内容,对属性

文件本质存放在磁盘上,访问文件,先写代码 -> 编译 -> exe -> 运行 ->访问文件,其本质是进程在访问文件!!!

        要向硬件写入,只有操作系统具备权利,而普通用户写入,则需要使用操作系统提供的文件类系统调用接口,而语言层面对系统调用接口进行了封装,由于不同操作系统平台提供的系统调用接口不同,语言把所有平台的代码都实现条件编译—动态裁剪,使其语言具有跨平台性!如果语言不提供对文件的系统接口的封装,所有访问文件的操作,都必须使用OS的接口,一旦使用系统接口,编写所谓的文件代码,就无法在其他平台安全运行,就不再具有跨平台性。

学习OS层面的文件系统,本质是为了更好的理解不同语言底层操作实际是相通的!

显示器是硬件,而printf向显示器打印,本质也是一种写入!

Linux下,一切皆文件,又该如何理解?

文件而言:

曾经理解的文件,站在写程序的角度,将文件打开,加载到内存,进行read,write

显示器:printf,cout -> 一种write

键盘:scanf,cin -> 一种read

普通文件 -> fopen/fread -> 你的进程内部(内存) -> fwrite -> 文件中

                         input                                        output

站在系统的角度,能够被input读取,或者output写出的设备就叫做文件!

狭义的文件——普通磁盘文件

广义上的文件——显示器,键盘,网卡,声卡,显卡,磁盘。几乎所有外色,都可以称之为文件

1. C文件接口

打开文件:fopen

打开方式: r,r+,w,w+,a,a+...

fopen以当前路径打开文件或者相对路径打开文件

当前路径默认是:当一个进程运行起来,在其pcb结构体中,没个进程都会记录当前所处的工作路径—cwd(创建工作的目录)即为该进程的当前路径!

1.1 写文件

//打开文件

FILE * fopen ( const char * filename, const char * mode );

//关闭文件

int fclose ( FILE * stream );

//fwrite:

size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );

buffer—要写入的数据  size—一个元素的大小 count—几个元素 stream—文件流

#include <stdio.h>
#include <string.h>
int main()
{
	FILE* fp = fopen("myfile", "w");
	if (!fp) {
		printf("fopen error!\n");
	}
	const char* msg = "hello Linux!\n";
	int count = 5;
	while (count--) {
		fwrite(msg, strlen(msg), 1, fp);
	}
	fclose(fp);
	return 0;
}

注意:这里写入字符串不计算'\0',因为字符串结束标志是C语言提供的,而不是操作系统所具有的!

1.2 读文件

//fread:

size_t fread( void *buffer, size_t size, size_t count, FILE *stream );

#include <stdio.h>
#include <string.h>
int main()
{
	FILE* fp = fopen("myfile", "r");
	if (!fp) {
		printf("fopen error!\n");
	}
	char buf[1024];
	const char* msg = "hello Linux!\n";
	while (1) {
		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;
}

1.3 输出信息到显示器

将写入的数据流入stdout,即输出到显示器

#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;
}

1.4 总结 and stdin & stdout & stderr

C默认会打开三个输入输出流,分别是stdin, stdout, stderr

仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针

打开文件的方式:

r          Open text file for reading. The stream is positioned at the beginning of the file.

r+        Open for reading and writing. The stream is positioned at the beginning of the file.

w         Truncate(缩短) file to zero length or create text file for writing. The stream is                          positioned at the beginning of the file.

w+       Open for reading and writing.The file is created if it does not exist, otherwise it is                    truncated. The stream is positioned at the beginning of the file.

a          Open for appending (writing at end of file). The file is created if it does not exist.                    The stream is positioned at the end of the file.

a+        Open for reading and appending (writing at end of file). The file is created if it does                not exist. The initial file position for reading is at the beginning of the file, but output              is always appended to the end of the file.

如上,是我们之前学的文件相关操作。还有 fseek ftell rewind 的函数,在C部分已经有所涉猎,请同学们自 行复习。

2. 系统调用文件I/O

操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问, 先来直接以代码的形式,实现和上面一模一样的代码!

2.1 系统接口使用示例

open close read write O_CREAT O_TRUNC O_WRONLY O_RDONLY O_RDWR...

写文件:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
	umask(0);
	int fd = open("myfile", O_WRONLY | O_CREAT, 0644);
	if (fd < 0) {
		perror("open");
		return 1;
	}
	int count = 5;
	const char* msg = "hello Linux!\n";
	int len = strlen(msg);
	while (count--) {
		write(fd, msg, len);//fd: 后续, msg:缓冲区首地址, len: 本次读取,期望写入多少个字节的数
		//据。 返回值:实际写了多少字节数据
	}
	close(fd);
	return 0;
}

读文件:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
	int fd = open("myfile", O_RDONLY);
	if (fd < 0) {
		perror("open");
		return 1;
	}
	const char* msg = "hello Linux!\n";
	char buf[1024];
	while (1) {
		ssize_t s = read(fd, buf, strlen(msg));//类比write
		if (s > 0) {
			printf("%s", buf);
		}
		else {
			break;
		}
	}
	close(fd);
	return 0;
}

在应用层看到一个很简单的动作,在系统接口层面甚至OS层面,可能要做非常多的动作!

2.2 接口介绍

open —— man 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: 要打开或创建的目标文件 
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。 
    参数: 
        O_RDONLY: 只读打开 
        O_WRONLY: 只写打开 
        O_RDWR : 读,写打开 
                 这三个常量,必须指定一个且只能指定一个 
        O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限 
        O_APPEND: 追加写
返回值:
 成功:新打开的文件描述符
 失败:-1

mode_t理解:直接 man 手册,比什么都清楚。

open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open。

2.3 open函数返回值

在认识返回值之前,先来认识一下两个概念: 系统调用 和 库函数

  • 上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)
  • 而, open close read write lseek 都属于系统提供的接口,称之为系统调用接口
  • 操作系统概念时,画的一张图

系统调用接口和库函数的关系,一目了然。 所以,可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。

3. 文件描述符fd及重定向

由上述系统调用open可知成功返回新打开的文件描述符(file descriptor)(fp)

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
    //未做差错处理
	int fd1 = open("myfile1", o_rdonly);
	int fd2 = open("myfile2", o_rdonly);
	int fd3 = open("myfile3", o_rdonly);
	int fd4 = open("myfile4", o_rdonly);
	
	printf("open sucess, fd: %d", fd1);
	printf("open sucess, fd: %d", fd2);
	printf("open sucess, fd: %d", fd3);
	printf("open sucess, fd: %d", fd4);
	close(fd1);
	close(fd2);
	close(fd3);
	close(fd4);
	return 0;
}

 而打开多个文件,发现其fd从3开始依次递增!0,1,2去哪里了?

可知文件描述符就是一个小整数,如何理解文件描述符?

3.1 0 & 1 & 2

  • Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.
  • 0,1,2对应的物理设备一般是:键盘,显示器,显示器
  • 所以输入输出还可以采用如下方式:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
	char buf[1024];
	ssize_t s = read(0, buf, sizeof(buf));
	if (s > 0) {
		buf[s] = 0;
		write(1, buf, strlen(buf));
		write(2, buf, strlen(buf));
	}
	return 0;
}

3.2 文件描述符fd的理解

FILE* fopen(const char* path, const char* mode);

C标准库提供的文件操作可知,FILE是一个由C标准库提供的结构体

由上述可知,C语言库函数内部一定封装了系统调用接口,lib在系统调用之上,站在系统角度,操作系统并不认识C语言内部的FILE结构体,只认识fd,因此FILE结构体内部,必定封装了fd!!

所以,fd是什么?

        进程想要访问文件,必须先打开文件

        一般而言,进程 : 打开文件 = 1 :n

文件要被访问,前提是加载到内存中,才能直接被访问!

可知一个进程可以打开多个文件,多个进程运行时,会存在大量的被打开的文件,所以,操作系统需要把这些被各个进程打开的文件管理起来,如何管理?先描述,在组织!!!

 

而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进 程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件

3.3 文件描述符Linux内核源码分析

文件:磁盘文件(未被打开的文件)                内存文件(被进程打开的文件)

        因此没每个进程的PCB中都一个文件表指针struct files_struct * files,指向一个struct files_struct文件表结构体,该文件表结构体内一定包含一个struct file* fd_array[]文件指针数组,其数组每个对应存取指向一个被打开文件结构体file的指针,file记录了该文件的所有属性及所有内容。

 

 fwrite() -> FILE* -> fd -> write -> write(fd, ...) -> 自己执行操作系统内部的write方法 -> 能找到进程的task_struct -> *files -> files_struct -> fd_array[] -> fd_array[fd] -> struct file -> 内存文件被找到 -> 操作

#include<stdio.h>
#include<stdlib.h>

struct File {
	void(*read_p)();
	void(*write_p)();
};

void readByKeyBoard() {
	cout << "从键盘读取" << endl;
}
void writeByKeyBoard() {
	cout << "nothing to do!" << endl;
}

void readByFile() {
	cout << "从文件读取" << endl;
}
void writeFile() {
	cout << "往文件写入" << endl;
}

void TestFile(struct File file) {
	file.read_p();
	file.write_p();
}

int main() {
	struct File fileByKB;
	fileByKB.read_p = readByKeyBoard;
	fileByKB.write_p = writeByKeyBoard;

	struct File fileByFILE;
	fileByFILE.read_p = readByFile;
	fileByFILE.write_p = writeFile;

	TestFile(fileByFILE);
	TestFile(fileByKB);
	return 0;
}

每个文件被加载到内存,其文件管理创建对应的file结构体,结构体内部会根据驱动填充相应的函数指针write,read等操作函数的地址,因为磁盘、显示器、键盘、网卡等不用硬件的读写方法不同,因此为了实现多态调用,C语言可以采用函数指针,实例化对象时填充不同的函数地址,以实现多态调用!

3.4 文件描述符的分配规则

直接看代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
 int fd = open("myfile", O_RDONLY);
 if(fd < 0){
 perror("open");
 return 1;
 }
 printf("fd: %d\n", fd);
 close(fd);
 return 0;
}

输出发现是 fd: 3

关闭0或者2,在看

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
	close(0);
	//close(2);
	int fd = open("myfile", O_RDONLY);
	if (fd < 0) {
		perror("open");
		return 1;
	}
	printf("fd: %d\n", fd);
	close(fd);
	return 0;
}

发现是结果是: fd: 0 或者 fd 2 可见,文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。

3.5 重定向

那如果关闭1呢?看代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
	close(1);
	int fd = open("myfile", O_WRONLY | O_CREAT, 00644);
	if (fd < 0) {
		perror("open");
		return 1;
	}
	printf("fd: %d\n", fd);
	fflush(stdout);

	close(fd);
	exit(0);
}

此时,发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有:>, >>, <

 重定向的本质,其实是在OS内部,更改fd对应的内容的指向!!!

3.6 使用 dup2 系统调用

函数原型如下:

#include<unistd.h> 

        int dup2(int oldfd, int newfd);

这里的oldfd copy 给 newfd,必要时会释放oldfd

 

代码如下:

#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;
}

 

4. 在minishell中添加重定向功能

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<assert.h>

#define NUM 1024
#define SIZE 32
char cmd_line[NUM];

void dealStr(char* str, char** argv) {
	char* dealstr = NULL;
	const char* sep = " ";
	size_t i = 0;
	for (dealstr = strtok(str, sep); dealstr != NULL; dealstr = strtok(NULL, sep)) {
		if (i < SIZE) {
			argv[i] = dealstr;
			i++;
		}
	}
    if(strcmp(argv[0], "ls") == 0){
        argv[i] = (char*)"--color=auto";
    }
	argv[++i] = NULL;
}

#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3
#define NONE_REDIR 0
int redir_status = NONE_REDIR;

char* CheckRedir(char* start){
    assert(start);
    //ls -a -l\0
    char* end = start + strlen(start) - 1;
    while(end >= start){
        if(*end == '>'){
            
            if(*(end - 1) == '>'){
                redir_status = APPEND_REDIR;
                *(end-1) = '\0';
                end++;
                break;
            }
            redir_status = OUTPUT_REDIR;
            *end = '\0';
            end++;
            break;
            //ls -a -l>myfile.txt
            //ls -a -l>>myfile.txt

        }else if(*end == '<'){
            redir_status = INPUT_REDIR;
            *end = '\0';
            end++;
            break;
        }else{
            end--;
        }
    }
    if(end >= start){
        return end;//要打开文件
    }else{
        return NULL;
    }
}

//环境变量保存的是地址,而拿不到环境变量本质是地址内容被清空,MY_VAL=NULL
char buffer[64];

//shell运行原理:通过让子进程执行命令,父进程等待&&解析命令
int main(){
    //extern char** environ;
    while(1){
        //1.打印提示信息
        printf("[root@localhost myshell]#");
        fflush(stdout);
        //2.获取用户输入
        memset(cmd_line, '\0', sizeof cmd_line);
        char *g_argv[SIZE] = { NULL };
        if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL){
            continue;
        }
        cmd_line[strlen(cmd_line) - 1] = '\0';
        //printf("echo:%s\n", cmd_line);
        //"ls -a -l > log.txt" -> "ls -a -l\0log.txt"
        char* sep = CheckRedir(cmd_line);
        
        //3.命令行字符串分割
        dealStr(cmd_line, g_argv);     
        
        //4.TODO 内置命令,让父进程(shell)自己执行的命令,叫做内置命令,内建命令
        // 内建命令本质就是shell中的一个函数调用
        if(strcmp("cd", g_argv[0]) == 0){
            //not child execute, father execute
            if(g_argv[1] != NULL)
                chdir(g_argv[1]);
            continue;
        }
        if(strcmp("export", g_argv[0]) == 0 && g_argv[1] != NULL){
            strcpy(buffer, g_argv[1]);
            putenv(buffer);
            //int checkPutenv = putenv(buffer);
            //if(checkPutenv == 0){
            //    printf("Putenv sucess\n");
            //    //for(size_t i = 0; environ[i]; i++){
            //        //printf("%s\n", environ[i]);
            //   // }
            //}else{
            //    printf("Putenc fail\n");
            //}
            continue;
        }

        //5.fork
        pid_t id = fork();
        if(id < 0){
            perror("fork");
            exit(1);
        }else if(id == 0){
            if(sep != NULL){
                int fd = -1;
                switch(redir_status){
                    case INPUT_REDIR:
                        fd = open(sep, O_RDONLY);
                        dup2(fd, 0);
                        break;
                    case OUTPUT_REDIR:
                        fd = open(sep, O_WRONLY | O_TRUNC | O_CREAT, 0666);
                        dup2(fd, 1);
                        break;
                    case APPEND_REDIR:
                        fd = open(sep, O_WRONLY | O_APPEND);
                        dup2(fd, 1);
                        break;
                    default:
                        printf("bug?\n");
                        break;
                }
            }

            //printf("getenv : %s\n", getenv("MY_VAL"));
            //execvpe(g_argv[0], g_argv, environ);
            //子进程继承父进程环境变量
            execvp(g_argv[0], g_argv);
            exit(1);
        }
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        if(ret > 0){
            printf("exit code:%d -> result:%s\n",WEXITSTATUS(status), strerror(WEXITSTATUS(status)));
        }else{
            printf("wait fail!\n");
            exit(1);
        }
    }//end while
    return 0;
}

5. 有关FILE缓冲区及Linux一切皆文件理解

Linux设计则学——一切皆文件——体现在操作系统的软件设计层面的

5.1 一切皆文件及C语言实现运行时多态

Linux是C语言写的!如何用C语言实现面向对象,甚至是运行时多态?

可以使用结构体及函数指针实现面向对象!!!及运行时多态!!!

在Linux下,根据冯诺依曼体系结构,大部分硬件都输入IO设备,IO设备主要用于输入输出,因此根据其驱动层提供的读写方法,即可实现一切皆文件的概念!

 所有的底层设备,都可以有自己的read和write方法的具体实现,但是,其具体的代码一定是不一样的!而通过函数指针使其根据硬件指向不同的读写方法,在调用时,就像在使用同一个函数!

5.2 缓冲区

1. 什么是缓冲区? 

        就是一段内存空间(这个空间社提供? 用户(char buffer[64], scanf(buffer))? 语言? OS?)

2. 为什么要有缓冲区

        提高整机效率,主要是为了提高用户的响应速度!

写透模式(WT模式): 只要写就刷新,会进行频繁的IO操作,成本高,且效率慢!

回写模式(WB模式):提供缓冲区,定义刷新策略,IO操作次数降低,效率高!

缓冲区的刷新策略:

        1. 立即刷新

        2. 行刷新(行缓冲)

        3. 满刷新(全缓冲)

特殊情况:

        1. 用户强制刷新(fflush)

        2. 进程退出

缓冲策略 = 一般 + 特殊, 缓冲策略是可控制的!

一般而言:行缓冲设备文件 —— 显示器           全缓冲设备文件 —— 磁盘文件

所有的设备,永远都倾向于全缓冲! —— 缓冲区满了,才刷新 --> 需要更少次的IO操作 --> 更少次的外设访问 --> 提高效率!

和外部设备进行IO的时候,数据量的大小不是主要矛盾,而和外设预备IO的过程是最耗费时间的!其他刷新策略是结合具体情况所做的妥协!

显示器:需要直接给用户看的,一方面照顾效率,一方面照顾用户体验,极端情况,是可以自定义规则的!

5.3 C标准库FILE结构体及缓冲区

因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。 所以C库当中的FILE结构体内部,必定封装了fd。

来段代码在研究一下:

#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 printf
hello fwrite
hello write

但如果对进程实现输出重定向呢? ./hello > file , 我们发现结果变成了:

hello write
hello printf
hello fwrite
hello printf
hello fwrite

我们发现 printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用)。为什么呢?肯定和 fork有关!

  • 一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
  • printf fwrite 库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据 的缓冲方式由行缓冲变成了全缓冲。
  • 而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后
  • 但是进程退出之后,会统一刷新,写入文件当中。
  • 但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的 一份数据,随即产生两份数据。
  • write 没有变化,说明没有所谓的缓冲。

综上: printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区, 都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。 那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统 调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是 C,所以由C标准库提供。

如果有兴趣,可以看看FILE结构体:

在 / usr / include / libio.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;
#ifdef _IO_USE_OLD_IO_FILE
};

5.4 大致模拟缓冲区设计

#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<assert.h>
#include<stdlib.h>

#define NUM 1024
typedef struct MyFILE_{
    int fd;
    char buffer[NUM];
    int end;//当前缓冲区结尾
}MFILE;

MFILE* fopen_(const char* pathname, const char* mode){
    assert(pathname && mode);
    MFILE* fp = NULL;

    if(strcmp(mode, "r") == 0){

    }else if(strcmp(mode, "r+") == 0){

    }else if(strcmp(mode, "w") == 0){
        int fd = open(pathname, O_WRONLY | O_CREAT | O_TRUNC, 0666);
        if(fd >= 0){
            fp = (MFILE*)malloc(sizeof(MFILE));
            memset(fp, 0, sizeof(MFILE));
            fp->fd = fd;
        }
    }else if(strcmp(mode, "w+") == 0){

    }else if(strcmp(mode, "a") == 0){

    }else if(strcmp(mode, "a+") == 0){

    }else{

    }

    return fp;
}

void fputs_(const char* message, MFILE* fp){
    assert(message && fp);
    strcpy(fp->buffer + fp->end, message);
    fp->end += strlen(message);

    //for debug
    fprintf(stderr, "%s\n", fp->buffer);//暂时未刷新

    //制定刷新策略,刷新策略是由谁来执行的呢?
    //答案是用户通过执行C标准库中的代码逻辑,来完成刷新
    //效率提高体现,几行代码就减少频繁的IO操作执行次数(注:不是数据量)
    //stdin
    if(fp->fd == 0){

    //stdout
    }else if(fp->fd == 1){
        if(fp->buffer[fp->end-1] == '\n'){
//            //for debug
//            fprintf(stderr, "fllush : %s", fp->buffer);

            write(fp->fd, fp->buffer, fp->end);
            fp->end = 0;
        }

    //stderr
    }else if(fp->fd == 2){

    }else{

    }
}

void fflush_(MFILE* fp){
    assert(fp);
    if(fp->end != 0){
        //暂且认为刷新了 -- 其实是把数据写到了内核中
        //如果想将数据由内核刷新到外设,系统调用sync-syncfs
        write(fp->fd, fp->buffer, fp->end);
        syncfs(fp->fd);//将数据写入磁盘
        fp->end = 0;
    }
}

void fclose_(MFILE* fp){
    assert(fp);
    fflush_(fp);
    close(fp->fd);
    free(fp);
}
int main(){
    //close(1);
    MFILE* fp = fopen_("./log.txt", "w");
    if(fp == NULL){
        printf("open file error\n");
        return 1;
    }

    fputs_("one:hello world\n", fp);
    //fputs_("two:hello world\n", fp);
    //fputs_("three:hello world", fp);
    //fputs_("four:hello world\n", fp);
    //fputs_("five:hello world", fp);
    //行刷新策略 one one:two tree tree:four five
    
    fork();
    //fork时,上下文数据写时拷贝,在缓冲区的one刷新两次

    fclose_(fp);

    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/69055.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

记录--前端重新部署如何通知用户

这里给大家分享我在网上总结出来的一些知识&#xff0c;希望对大家有所帮助 1. 场景 前端构建完上线&#xff0c;用户还停留还在老页面&#xff0c;用户不知道网页重新部署了&#xff0c;跳转页面的时候有时候js连接hash变了导致报错跳不过去&#xff0c;并且用户体验不到新功能…

SAP Fiori 问题收集

事务代码篇 启动工作台&#xff1a;/N/UI2/FLP 错误日志&#xff1a; /n/IWFND/ERROR_LOG 服务清单&#xff1a; /n/IWFND/MAINT_SERVICE 创建语义对象&#xff1a;/N/UI2/SEMOBJ 创建目录&#xff1a;/N/UI2/FLPD_CONF&#xff08;cross-client&#xff09;或 /N/UI2…

【ES】笔记-箭头函数的实践于应用场景

箭头函数的实践于应用场景 需求-1 点击 div 2s后颜色变成[粉色]从数组中返回偶数的元素 需求-1 点击 div 2s后颜色变成[粉色] html <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport…

uniapp之当你问起“tab方法触发时eventchange也跟着触发了咋办”时

我相信没有大佬会在这个问题上卡两个小时吧&#xff0c;记下来大家就当看个乐子了。 当时问题就是&#xff0c;点击tab头切换的时候&#xff0c;作为tab滑动事件的eventchange同时触发了&#xff0c;使得接口请求了两次 大概是没睡好&#xff0c;我当时脑子老想着怎么阻止它冒…

怎么系统的学习机器学习、深度学习?当然是看书了

目录 前言 内容简介 学完本书&#xff0c;你将能够 作者简介 本书目录 京东自购链接 前言 近年来&#xff0c;机器学习方法凭借其理解海量数据和自主决策的能力&#xff0c;已在医疗保健、 机器人、生物学、物理学、大众消费和互联网服务等行业得到了广泛的应用。自从Ale…

【GPT-3 】创建能写博客的AI工具

一、说明 如何使用OpenAI API&#xff0c;GPT-3和Python创建AI博客写作工具。 在本教程中&#xff0c;我们将从 OpenAI API 中断的地方继续&#xff0c;并创建我们自己的 AI 版权工具&#xff0c;我们可以使用它使用 GPT-3 人工智能 &#xff08;AI&#xff09; API 创建独特的…

uniapp 微信小程序 封装公共的请求js(api版本)

一、新建api文件夹 在项目目录下创建api文件夹&#xff0c;内放files跟index.js文件夹&#xff0c;files文件夹内放每个页面对应的js请求接口 1、index.js /*** api接口的统一出口*/ const api {}; const requireComponent require.context(./files, false, /\.js$/) requi…

4个简化IT服务台任务的ChatGPT功能

最近几个月&#xff0c;ChatGPT 风靡全球&#xff0c;这是一个 AI 聊天机器人&#xff0c;使用户能够生成脚本、文章、锻炼图表等。这项技术在各行各业都有无穷无尽的应用&#xff0c;在本文中&#xff0c;我们将研究这种现代技术如何帮助服务台团队增强服务交付和客户体验。 什…

林【2019】

关键字&#xff1a; 哈夫曼树权值最小、哈夫曼编码、邻接矩阵时间复杂度、二叉树后序遍历、二叉排序树最差时间复杂度、非连通无向图顶点数&#xff08;完全图&#xff09;、带双亲的孩子链表、平衡二叉树调整、AOE网关键路径 一、判断 二、单选 三、填空 四、应用题 五、算…

Blazor 简单组件(0):简单介绍

文章目录 前言说明环境安装 前言 Blazor 这个技术还是比较新&#xff0c;相关的UI组件还在完善&#xff0c;我这里提供一下我个人的组件开发。 说明 本UI组件是基于BootstrapBlazor(以下简称BB)开发。 BootstrapBlazor 文档 环境安装 C#小轮子&#xff1a;Visual Studio自…

第一百二十三天学习记录:C++提高:STL-vector容器(下)(黑马教学视频)

vector插入和删除 功能描述&#xff1a; 对vector容器进行插入、删除操作 函数原型&#xff1a; push_back(ele); //尾部插入元素ele pop_back(); //删除最后一个元素 insert(const_iterator pos, ele); //迭代器指向位置pos插入元素ele insert(const_iterator pos, int cou…

(el-switch)操作(不使用 ts):Element-plus 中 Switch 将默认值修改为 “true“ 与 “false“(字符串)来控制开关

Ⅰ、Element-plus 提供的 Switch 开关组件与想要目标情况的对比&#xff1a; 1、Element-plus 提供 Switch 组件情况&#xff1a; 其一、Element-ui 自提供的 Switch 代码情况为(示例的代码)&#xff1a; // Element-plus 自提供的代码&#xff1a; // 此时是使用了 ts 语言环…

山东布谷科技直播系统源码热点分析:不同芯片实现高质量编码与渲染视频的GPU加速功能

在现代科技的迅猛发展下&#xff0c;直播系统源码平台被开发搭建出来&#xff0c;为人们的生活方式带来了很大的改变&#xff0c;直播系统源码平台的好友、短视频、直播、社区等功能让很多人越来越热衷于去在平台上刷视频、看直播、分享生活。用户的喜爱也督促了直播系统源码平…

Vue3 Props组件简单应用(子组件获取父组件数据)

去官网学习→Props | Vue.js 运行示例&#xff1a; 代码&#xff1a;App.vue <template><img alt"Vue logo" src"./assets/logo.png"><!-- 传递数据 key value--><Mycomponent :dataTest"content" :dataNmub&…

Java类型转换

总是忘&#xff0c;总是记混&#xff0c;气气气&#xff01; 基本类型 4整型、2浮点型、1布尔、1字符 关键字大小取值范围包装类型byte8-27~27-1Byteshort16-215~215-1Shortint32-231~231-1Integerlong64-263~263-1Longfloat323.4e-38~3.4e38Floatdouble641.7e-38~1.7e38Dou…

linux Ubuntu 更新镜像源、安装sudo、nvtop、tmux

1.更换镜像源 vi ~/.pip/pip.conf在打开的文件中输入: pip.conf [global] index-url https://pypi.tuna.tsinghua.edu.cn/simple按下:wq保存并退出。 2.安装nvtop 如果输入指令apt install nvtop报错&#xff1a; E: Unable to locate package nvtop 需要更新一下apt&a…

tomcat多实例与动静分离

多实例&#xff1a; 在一台服务器上配置多台tomcat服务 配置 tomcat 环境变量 修改 tomcat2 中的 server.xml 文件&#xff0c;要求各 tomcat 实例配置不能有重复的端口号 vim /usr/local/tomcat/tomcat2/conf/server.xml<Server port"8006" shutdown"SHUT…

数据结构:堆的实现(C实现)

个人主页 &#xff1a; 个人主页 个人专栏 &#xff1a; 《数据结构》 《C语言》 文章目录 一、堆二、实现思路1. 结构的定义2. 堆的构建 (HeapInit)3. 堆的销毁 (HeapDestroy)4. 堆的插入 (HeapPush)5. 堆的删除 (HeapPop)6. 取堆顶的数据 (HeapTop)7. 堆的数据个数 (HeapSize…

CentOS7安装Maven详细教程

&#x1f60a; 作者&#xff1a; Eric &#x1f496; 主页&#xff1a; https://blog.csdn.net/weixin_47316183?typeblog &#x1f389; 主题&#xff1a;CentOS7安装Maven详细教程 ⏱️ 创作时间&#xff1a; 2023年08月06日 第一步&#xff1a;上传或下载安装包&#x…

Oracle数据迁移

问题描述&#xff1a; oracle数据库的所有表结构、数据、索引等需要需从测试库迁移到正式库。 解决步骤&#xff1a; oracle数据库迁移&#xff0c;主要通过expdp从测试库所在的源服务器将指定的数据表或数据源导出为一个或多个数据文件&#xff08;.dmp文件&#xff09;&…