当前位置: 首页 > news >正文

Linux系统编程—基础IO

第一章:理解"文件"

1-1 狭义理解

  • 文件在磁盘里
  • 磁盘是永久性存储介质,因此文件在磁盘上的存储是永久性的
  • 磁盘是外设(即是输出设备也是输入设备)
  • 磁盘上的文件 本质是对文件的所有操作,都是对外设的输入和输出 简称 IO

1-2 广义理解

Linux 下一切皆文件(键盘、显示器、网卡、磁盘…… 这些都是抽象化的过程)

1-3 文件操作的归类认知

  • 对于 0KB 的空文件是占用磁盘空间的
  • 文件是文件属性(元数据)和文件内容的集合(文件 = 属性(元数据)+ 内容)
  • 所有的文件操作本质是文件内容操作和文件属性操作

1-4 系统角度

  • 对文件的操作本质是进程对文件的操作
  • 磁盘的管理者是操作系统
  • 文件的读写本质不是通过 C 语言 / C++ 的库函数来操作的(这些库函数只是为用户提供方便),而是通过文件相关的系统调用接口来实现的

第二章:回顾C文件接口

2-1 hello.c打开文件

#include <stdio.h>
int main() {FILE* fp = fopen("myfile", "w");if (!fp) printf("fopen error!\n");while (1);fclose(fp);return 0;
}

打开的myfile文件在哪个路径下?

  • 在程序的当前路径下,那系统怎么知道程序的当前路径在哪里呢?

验证更改当前进程的cwd,就可以把文件新建到其他目录

int main() {chdir("/home/sxy");printf("pid:%d\n", getpid());//如果文件创建时有绝对路径或相对路径,那么就在对应路径下创建//否则就是在其进程当前路径下创建对FILE* fp = fopen("log.txt", "w");if (fp == NULL) {perror("fopen");return 1;}fclose(fp);sleep(100);return 0;
}

可以使用 ls /proc/[进程id] -l 命令查看当前正在运行进程的信息:

[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⼿册关于该函数的说明ssize_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>
int main(int argc, char* argv[]) {if (argc != 2) {printf("argv error!\n");return 1;}FILE* fp = fopen(argv[1], "r");if (!fp) {printf("fopen error!\n");return 2;}char buf[1024];while (1) {int s = fread(buf, 1, sizeof(buf), fp);if (s > 0) {buf[s] = 0;printf("%s", buf);}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

  • C默认会打开三个输入输出流,分别是stdin, stdout, stderr
  • 仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针
#include <stdio.h>
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;

2-6 打开文件的方式

r	Open text file for reading.The stream is positioned at the beginning of the file.r+	Open for readingand 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 readingand 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 readingand appending(writing at end of file).The file is created if it does not exist.The initial file positionfor reading is at the beginning of the file,but output is always appended to the end of the file.

第三章:系统文件I/O

打开文件的方式不仅仅是fopen,ifstream等流式,语言层的方案,其实系统才是打开文件最底层的方案。不过,在学习系统文件IO之前,先要了解下如何给函数传递标志位,该方法在系统文件IO接口中会使用到:

3-1 一种传递标志位的方法

#include <stdio.h>
#define ONE 0001 //0000 0001
#define TWO 0002 //0000 0010
#define THREE 0004 //0000 0100
void func(int flags) {if (flags & ONE) printf("flags has ONE! ");if (flags & TWO) printf("flags has TWO! ");if (flags & THREE) printf("flags has THREE! ");printf("\n");
}
int main() {func(ONE);func(THREE);func(ONE | TWO);func(ONE | THREE | TWO);return 0;
}

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

3-2 hello.c 写文件

#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 bit!\n";int len = strlen(msg);while (count--) {//fd: 后面讲, msg:缓冲区首地址, len: 本次读取,//期望写入多少个字节的数据。 返回值:实际写了多少字节数据write(fd, msg, len);}close(fd);return 0;
}

3-3 hello.c读文件

#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 bit!\n";char buf[1024];while (1) {ssize_t s = read(fd, buf, strlen(msg));//类比writeif (s > 0) {printf("%s", buf);}else {break;}}close(fd);return 0;
}

3-4 接口介绍

#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。
write read close lseek ,类比C文件相关接口。

3-5 open函数返回值

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

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

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

3-6 文件描述符fd

通过对open函数的学习,我们知道了文件描述符就是一个小整数

3-6-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;
}

为什么要有文件描述符2

//一个程序运行时可能需要输出结果,但这个过程中可能会发生错误,并且期望发生错误时能输出错误信息。
//这两种信息不仅要能在显示器上看到,有时还要输出到文件中。所以就有显示器的两种文件描述符。
//正确的信息可以重定向到一个文件中,错误的信息可以重定向到另一个文件中。
int main() {//stdout -> 1    stderr -> 2fprintf(stdout, "hello normal message\n");fprintf(stdout, "hello normal message\n");fprintf(stdout, "hello normal message\n");fprintf(stdout, "hello normal message\n");fprintf(stderr, "hello error message\n");fprintf(stderr, "hello error message\n");fprintf(stderr, "hello error message\n");fprintf(stderr, "hello error message\n");return 0;
}

描述符1和2的区别

文件描述符1和2的作用与设计

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

对于以上原理结论我们可通过内核源码验证:

首先要找到 task_struct 结构体在内核中为位置,地址为: /usr/src/kernels/3.10.0-
1160.71.1.el7.x86_64/include/linux/sched.h (3.10.0-1160.71.1.el7.x86_64是内核版
本,可使用 uname -a 自行查看服务器配置, 因为这个文件夹只有一个,所以也不用刻意去分辨,内核版本其实也随意)

要查看内容可直接用vscode在windows下打开内核源代码
相关结构体所在位置
struct task_struct : /usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/sched.h

struct files_struct : /usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/fdtable.h

struct file : /usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/fs.h

3-6-2 文件描述符的分配规则

直接看代码:

#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数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。

#define filename "log.txt"int main() {//close(0);//没有关闭0,打印fd=3;关闭0后打印fd=0;//close(1);//关闭1,屏幕不输出信息,但下方代码依然能输出内容到文件//close(2);//关闭2,可以打印fd=2int fd = open(filename, O_CREAT|O_WRONLY|O_TRUNC, 0666);if (fd < 0) {perror("open: ");return 1;}printf("fd:%d\n", fd);const char* msg = "hello Linux\n";int cnt = 5;while (cnt) {write(fd, msg, strlen(msg));cnt--;}close(fd);return 0;
}

3-6-3 重定向

那如果关闭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。这种现象叫做输出重定向。常见的重定向有:>, >>, <

那重定向的本质是什么呢?

3-6-4 使用 dup2 系统调用

函数原型如下:

#include <unistd.h>
int dup2(int oldfd, int newfd);

示例代码

#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下标所表示内容,已经变成了myfile的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成输出重定向。那追加和输入重定向如何完成呢?

#define filename "log.txt"int main() {//int fd = open(filename, O_CREAT|O_WRONLY|O_TRUNC, 0666);//输出重定向int fd = open(filename, O_CREAT|O_WRONLY|O_APPEND, 0666);//追加重定向if (fd < 0) {perror("open: ");return 1;}//重定向dup2(fd, 1);close(fd);const char* msg = "hello Linux\n";int cnt = 5;while (cnt) {write(1, msg, strlen(msg));cnt--;}//输入重定向int fd = open(filename, O_RDONLY);if (fd < 0) {perror("open: ");return 1;}dup2(fd, 0);char inbuffer[1024];ssize_t s = read(0, inbuffer, sizeof(inbuffer) - 1);if (s > 0) {inbuffer[s] = '\0';printf("echo# %s\n", inbuffer);}close(fd);return 0;
}

3-6-5 在minishell中添加重定向功能

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <ctype.h>
#include <fcntl.h>#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define DELIM " \t"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 44#define NONE -1
#define IN_RDIR 0
#define OUT_RDIR 1
#define APPEND_RDIR 2int lastcode = 0;
int quit = 0;
extern char** environ;
char commandline[LINE_SIZE];
char* argv[ARGC_SIZE];
char pwd[LINE_SIZE];
char* rdirfilename = NULL; //重定向的名字
int rdir = NONE; //重定向的方式//自定义环境变量表
char myenv[LINE_SIZE];
//自定义本地变量表const char* getusername() { return getenv("USER"); }
const char* gethostname() { return getenv("HOSTNAME"); }
void getpwd() { getcwd(pwd, sizeof(pwd)); }//获取指令、文件名、重定向的符号
void check_redir(char* cmd) {// ls -al -n// ls -al -n >/</>> filename.txtchar* pos = cmd;while (*pos) {if (*pos == '>') {if (*(pos+1) == '>') { //两个>>是追加重定向*pos++ = '\0';//将两个>>置为\0*pos++ = '\0';while (isspace(*pos)) pos++;//跳过空格rdirfilename = pos;//pos不为空时,指向的就是文件名rdir = APPEND_RDIR;break;}else { *pos = '\0';pos++;while (isspace(*pos)) pos++;//跳过空格rdirfilename = pos;//pos不为空时,指向的就是文件名rdir = APPEND_RDIR;break;}}else if (*pos == '<') {// ls -a -l -n < filename.txt*pos = '\0';//将<置为\0pos++;while (isspace(*pos)) pos++;//跳过空格rdirfilename = pos;//pos不为空时,指向的就是文件名rdir = IN_RDIR;break;}else {}pos++;}
}void interact(char* cline, int size) {getpwd();printf(LEFT"%s@%s %s" RIGHT"" LABLE" ", getusername(), gethostname(), pwd);//scanf("%s", commandline);//读取到空格就结束,所以不能使用scanfchar* s = fgets(cline, size, stdin);assert(s);//assert在Debug下才有用,Release模式不执行(void)s;//消除编译器警告:有些编译器会对未使用的变量发出警告 //"abcd\n\0"输入字符后还会自动跟\n,不想要\n所以将其置为\0cline[strlen(cline)-1] = '\0';//消除\n//ls -a -l > myfile.txtcheck_redir(cline);
}int splitstring(char cline[], char* _argv[]) {int i = 0;//commandline -> "ls -a -l -n\0" -> "ls" "-a" "-l" "-n"_argv[i++] = strtok(cline, DELIM);while(_argv[i++] = strtok(NULL, DELIM)) ;//故意写的=,即每次取子串不为空就继续//strtok返回子串的起始地址,最后一次找不到返回NULL,恰好满足自定义命令行参数表的要求return i - 1;//总的参数个数还包含了NULL,所以实际有效参数个数是i-1
}void NormalExcute(char* _argv[]) {pid_t id = fork();if (id < 0) {perror("fork");return;}else if (id == 0) { //子进程执行命令//下面先做了重定向工作,接着又做了程序替换,不会影响吗?int fd = 0;if (rdir == IN_RDIR) { //输入重定向fd = open(rdirfilename, O_RDONLY);dup2(fd, 0);}else if (rdir == OUT_RDIR) { //输出重定向fd = open(rdirfilename, O_CREAT|O_WRONLY|O_TRUNC, 0666);dup2(fd, 1);}else if (rdir == APPEND_RDIR) { //追加重定向fd = open(rdirfilename, O_CREAT|O_WRONLY|O_APPEND, 0666);dup2(fd, 1);}//execvpe(_argv[0], _argv, environ); //执行成功,exit不执行;只有程序替换失败才会执行exitexecvp(_argv[0], _argv); //执行成功,exit不执行;只有程序替换失败才会执行exitexit(EXIT_CODE);}else {int status = 0;pid_t rid = waitpid(id, &status, 0);if (rid == id) {lastcode = WEXITSTATUS(status);//最后一个子进程的退出码}}
}int buildCommand(char* _argv[], int _argc) {if (_argc == 2 && strcmp(_argv[0], "cd") == 0) {chdir(_argv[1]);getpwd();sprintf(getenv("PWD"), "%s", pwd);return 1;}else if (_argc == 2 && strcmp(_argv[0], "export") == 0) {//直接将命令行参数字符串放入环境变量中。但环境变量字符串需要在整个程序运行期间保持有效,//而命令行参数可能会被后续的命令覆盖。//putenv((char*)_argv[1]);strcpy(myenv,_argv[1]);putenv(myenv);return 1;}else if (_argc == 2 && strcmp(_argv[0], "echo") == 0) {if (strcmp(_argv[1], "$?") == 0) {printf("%d\n", lastcode);lastcode = 0;}//echo $PATH -> _argv[1]是$PATH字符串首字符地址,解引用就是$else if (*_argv[1] == '$') {char* val = getenv(_argv[1] + 1);if(val) printf("%s\n", val);//+1就是跳过$}else printf("%s\n", _argv[1]);return 1;}//特殊处理lsif (strcmp(_argv[0], "ls") == 0) {_argv[_argc++] = "--color";//在命令行参数表最后加color选项_argv[_argc] = NULL;}return 0;
}int main() {while (!quit) {//1.rdir = NONE;rdirfilename = NULL;//2.交互问题,获取命令行interact(commandline, sizeof(commandline));//命令行提示符 //3.子串分割的问题,解析命令行int argc = splitstring(commandline, argv);if (argc == 0) continue;//4.指令的判断//debug//for(int i = 0; argv[i]; ++i) printf("[%d]:%s\n", i, argv[i]);//内建命令,本质就是一个shell内部的一个函数int n = buildCommand(argv, argc);//5.普通命令的执行if(!n) NormalExcute(argv);}return 0;
}

重定向和程序替换解释1

重定向和程序替换解释2

第四章:理解 “ 一切皆文件”

首先,在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下。

file_operation 就是把系统调用和驱动程序关联起来的关键数据结构,这个结构的每一个成员都对应着一个系统调用。读取 file_operation 中相应的函数指针,接着把控制权转交给函数,从而完成了Linux设备驱动程序的工作。

介绍完相关代码,一张图总结:

上图中的外设,每个设备都可以有自己的read、write,但一定是对应着不同的操作方法!!但通过struct file 下 file_operation 中的各种函数回调,让我们开发者只用file便可调取 Linux 系统中绝大部分的资源!!这便是“linux下一切皆文件”的核心理解。

什么是一切皆文件

怎么做到一切皆文件

Linux内核中进程如何与文件和设备交互的完整链条

第五章:缓冲区

5-1 什么是缓冲区

缓冲区是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。

上方代码解释

代码运行过程

5-2 为什么要引入缓冲区机制

读写文件时,如果不会开辟对文件操作的缓冲区,直接通过系统调用对磁盘进行操作(读、写等),那么每次对文件进行一次读写操作时,都需要使用读写系统调用来处理此操作,即需要执行一次系统调用。执行一次系统调用将涉及到 CPU 状态的切换,即从用户空间切换到内核空间,实现进程上下文的切换,这将损耗一定的 CPU 时间。频繁的磁盘访问会对程序的执行效率造成很大的影响。

为了减少使用系统调用的次数,提高效率,我们就可以采用缓冲机制。比如我们从磁盘里取信息,在磁盘文件进行操作时,可以一次从文件中读出大量的数据到缓冲区中,以后对这部分的访问就不需要再使用系统调用了,等缓冲区的数据取完后再去磁盘中读取。这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作远快于对磁盘的操作,故应用缓冲区可大大提高计算机的运行速度。

又比如,我们使用打印机打印文档,由于打印机的打印速度相对较慢,我们先把文档输出到打印机相应的缓冲区,打印机再自行逐步打印,这时我们的 CPU 可以处理别的事情。可以看出,缓冲区就是一块内存区,它用在输入输出设备和 CPU 之间,用来缓存数据。它使得低速的输入输出设备和高速的 CPU 能够协调工作,避免低速的输入输出设备占用 CPU,解放出 CPU,使其能够高效率工作。

缓冲区的作用

5-3 缓冲类型

  • 全缓冲区:全缓冲的方式要求在缓冲区被填满之后,才会进行一次 I/O 系统调用操作。对于磁盘文件的读写操作,通常使用全缓冲方式。这种方式可以显著减少系统调用次数,从而提高整体效率。
  • 行缓冲区:在行缓冲情况下,当输入或输出中遇到换行符时,标准 I/O 库函数就会执行一次系统调用。当所操作的流涉及一个终端(例如标准输入 stdin 或标准输出 stdout)时,通常使用行缓冲方式。除此之外,如果行缓冲区被填满,即使没有遇到换行符,也会强制触发系统调用。默认情况下,行缓冲区的大小一般为 1024 字节。
  • 无缓冲区:无缓冲方式下,标准 I/O 库不会对数据进行缓存,而是每次直接通过系统调用进行输入输出操作。标准错误流 stderr 就是典型的不带缓冲区的流,这样做的目的是为了保证错误信息能够立即显示出来,避免延迟。

除了上述列举的默认刷新方式,下列特殊情况也会引发缓冲区的刷新:

  1. 缓冲区满时;
  2. 执行flush语句;
  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;
}

我们本来想使用重定向思维,让本应该打印在显示器上的内容写到“log.txt”文件中,但我们发现,程序运行结束后,文件中并没有被写入内容:

[hyb@VM-8-12-centos buffer]$ ./myfile 1
[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]$

这是由于我们将1号描述符重定向到磁盘文件后,缓冲区的刷新方式成为了全缓冲。而我们写入的内容并没有填满整个缓冲区,导致并不会将缓冲区的内容刷新到磁盘文件中。怎么办呢?可以使用fflush强制刷新下缓冲区。

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

还有一种解决方法,刚好可以验证一下stderr是不带缓冲区的,代码如下:

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

这种方式便可以将2号文件描述符重定向至文件,由于stderr没有缓冲区,“hello world”不用fflash就可以写入文件:

[hyb@VM-8-12-centos buffer]$ ./myfile
[hyb@VM-8-12-centos buffer]$ cat log.txt
hello world: Success

5-4 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结构体:

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 0int _blksize;
#elseint _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
};

代码补充解释1

代码补充解释2

FILE补充解释1

FILE补充解释2

5-5 简单设计⼀下libc库

Mystdio.h

#ifndef __MYSTDIO_H__
#define __MYSTDIO_H_#include <string.h>#define SIZE 1024
#define FLUSH_NOW 1
#define FLUSH_LINE 2
#define FLUSH_ALL 4typedef struct IO_FILE{int fileno;int flag;//刷新模式//char inbuffer[SIZE];//int in_pos;char outbuffer[SIZE];int out_pos;//指向输出缓冲区准备刷新的字符
}_FILE;_FILE* _fopen(const char* filename, const char* flag);
int _fwrite(_FILE* fp, const char* s, int len);
void _fclose(_FILE* fp);#endif

Mystdio.c

#include "Mystdio.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>#define FILE_MODE 0666_FILE* _fopen(const char* filename, const char* flag) {assert(filename);assert(flag);//"w", "a", "r"int f = 0;//标志位int fd = -1;if (strcmp(flag, "w") == 0) {f = (O_CREAT|O_WRONLY|O_TRUNC);fd = open(filename, f, FILE_MODE);}else if (strcmp(flag, "a") == 0) {f = (O_CREAT|O_WRONLY|O_APPEND);fd = open(filename, f, FILE_MODE);}else if (strcmp(flag, "R") == 0) {f = O_RDONLY;fd = open(filename, f);}else return NULL;if (fd == -1) return NULL;//成功打开文件后,创建FILE结构体_FILE* fp = (_FILE*)malloc(sizeof(_FILE));if (fp == NULL) return NULL;fp->fileno = fd;//fp->flag = FLUSH_LINE;//行刷新模式fp->flag = FLUSH_ALL;fp->out_pos = 0;//输出缓冲区已刷新字符return fp;
}int _fwrite(_FILE* fp, const char* s, int len) {//"abcd\n"  memcpy(&fp->outbuffer[fp->out_pos], s, len);//没有做异常处理,也不考虑局部问题fp->out_pos += len;if (fp->flag & FLUSH_NOW) {write(fp->fileno, fp->outbuffer, fp->out_pos);fp->out_pos = 0;}else if (fp->flag & FLUSH_LINE) {if (fp->outbuffer[fp->out_pos-1] == '\n') { //不考虑其他情况write(fp->fileno, fp->outbuffer, fp->out_pos);fp->out_pos = 0;}}else if (fp->flag & FLUSH_ALL) {if (fp->out_pos == SIZE) {write(fp->fileno, fp->outbuffer, fp->out_pos);fp->out_pos = 0;}}return len;}void _fflush(_FILE* fp) {if (fp->out_pos > 0) {write(fp->fileno, fp->outbuffer, fp->out_pos);fp->out_pos = 0;}
}void _fclose(_FILE* fp){if (fp == NULL) return;_fflush(fp);close(fp->fileno);free(fp);
}

main.c

#include "Mystdio.h"
#include <unistd.h>#define myfile "test.txt"int main() {_FILE* fp = _fopen(myfile, "a");if (fp == NULL) return 1;const char* msg = "hello world\n";int cnt = 10;while (cnt) {_fwrite(fp, msg, strlen(msg));sleep(1);cnt--;}_fclose(fp);return 0;
}

作业

1. 文件描述符的理解

在进程中每打开一个文件,都会创建有相应的文件描述信息struct file,这个描述信息被添加在pcb的struct files_struct中,以数组的形式进行管理,随即向用户返回数组的下标作为文件描述符,用于操作文件

2. 以下描述正确的是 [多选]

A.程序中打开文件所返回的文件描述符, 本质上在PCB中是文件描述符表的下标
B.多个文件描述符可以通过dup2函数进行重定向后操作同一个文件
C.在进程中多次打开同一个文件返回的文件描述符是一致的
D.文件流指针就是struct _IO_FILE结构体, 该结构体当中的int _fileno 保存的文件描述符, 是一对一的关系

答案:ABC

  • A选项正确:文件描述符实际上是内核中文件描述信息表中数组下标,用户根据下标在进程pcb中的文件描述信息表中找到相应文件描述信息,实现对文件的操作;
  • B选项正确:不同信息表数组下标的位置可以存放相同的文件描述信息结构指针,dup2重定向的本质原理,就是改变对应位置的文件信息而改变操作对象文件的。
  • C选项错误:文件描述符表中遵循最小未使用分配规则,也就是从表中找寻最小的没有被使用的位置进行存储,因此并不保证多次打开会使用同一个文件描述符
  • D选项正确:我们见到的FILE* 文件流指针,其实就是_IO_FILE的类型重定义,其中封装包含了文件描述符,因此一个文件流指针一定对应有一个文件描述符。

3. 以下描述正确的是

A.文件描述符和文件流指针没有任何关系
B.文件流指针结构中封装了文件描述符
C.通过open打开文件返回的FILE *fp可以直接使用read读取数据
D.通过open打开文件返回的FILE *fp可以直接使用fread读取数据

答案:B
文件流指针是标准库IO操作句柄,是一个FILE*结构体指针,其中内部封装有文件描述符,其对应的操作接口有:fopen, fread, fwrite, fseek, fclose...
文件描述符是系统调用接口操作句柄,是一个非负整数,期对应的操作接口有: open, read, write, lseek, close...
根据以上理解:

  • A选项错误,文件流指针中封装有文件描述符,他们是封装包含的关系
  • B选项正确
  • C和D选项错误,open是系统调用接口,返回的是文件描述符,并非文件流指针

4. 以下关于标准输入输出错误的描述正确的是

A.在文件描述符中0表示标准输出,1表示标准错误,2表示标准输入
B.在文件描述符中0表示标准错误,1表示标准输出,2表示标准输入
C.在文件描述符中0表示标准输入,1表示标准输出,2表示标准错误
D.在文件描述符中0表示标准输出,1表示标准输入,2表示标准错误

答案:C
在课堂中,我们讲到,一个进程启动后,默认会打开三个文件,分别针对,标准输入文件,标准输出文件,以及标准错误文件,而这三者文件分别对应的描述符为:
标准输入=0;    标准输入=1;    标准错误=2;
根据以上理解,正确选项为C选项。

5.  Linux下两个进程可以同时打开同一个文件,这时如下描述错误的是:

A.两个进程中分别产生生成两个独立的fd
B.两个进程可以任意对文件进行读写操作,操作系统并不保证写的原子性
C.进程可以通过系统调用对文件加锁,从而实现对文件内容的保护
D.任何一个进程删除该文件时,另外一个进程会立即出现读写失败
E.两个进程可以分别读取文件的不同部分而不会相互影响
F.一个进程对文件长度和内容的修改另外一个进程可以立即感知

答案:D
经过上课所学我们可以知道,在一个进程中打开一个文件,会在进程内生成文件的描述信息结构,并将其地址添加到pcb中的文件描述信息数组中,最终返回所在位置下标作为文件描述符

  • A选项正确,进程数据独有,各自有各自的文件描述信息表,因此各自打开文件会有自己独立的描述信息添加在各自信息表的不同位置,因此fd各自也相互独立
  • B选项正确,两个进程打开同一个文件,但是各有各的文件描述信息以及读写位置,互不影响,因此多个进程同时读写有可能会造成穿插覆盖的情况(原子性操作,被认为是一次性完成的操作,操作过程中间不会被打断,通常以此表示操作的安全性)
  • C选项正确,文件锁就是用于保护对文件当前的操作不会被打断,就算时间片轮转,因为已经对文件加锁,其他的进程也无法对文件内容进行操作,从而保护在本次文件操作过程是安全的。
  • D选项错误,删除文件实际上只是删除文件的目录项,文件的数据以及inode并不会立即被删除,因此若进程已经打开文件,文件被删除时,并不会影响进程的操作,因为进程已经具备文件的描述信息(可以编写代码进行尝试,在文件打开后,外界删除文件,然后看进程中是否还可以继续写入或读取数据)
  • E选项正确,如果仅仅是读取文件内容,两个不同进程其实都有自己各自的描述信息和读写位置,因此可以同时读取文件数据而不会受到对方的影响。
  • F选项正确,因为文件内容的修改是直接反馈至磁盘文件系统中的,因此当文件内容被修改,其他进程因为也是针对磁盘数据的操作,因此可以立即感知到(可以写代码尝试一个进程打开文件后,等其他进程修改了内容后然后再读取文件数据进行测试)

针对以上理解,描述不正确的只有D选项

6. 请简述重定向的实现原理:(简答题)

每个文件描述符都是一个内核中文件描述信息数组的下标,对应有一个文件的描述信息用于操作文件,而重定向就是在不改变所操作的文件描述符的情况下,通过改变描述符对应的文件描述信息进而实现改变所操作的文件

7. 以下代码的功结果是

void func() {int fd = open("./tmp.txt", O_RDWR | O_CREAT, 0664);if (fd < 0) {return -1;}dup2(fd, 1);printf("hello bit");return 0;
}

A.将hello bit打印到终端显示
B.将hello bit 写入到tmp.txt中
C.将hello bit 打印到终端显示并且写入tmp.txt文件中
D.既不打印,也没有写入到文件中

答案:B

void func() {int fd = open("./tmp.txt", O_RDWR | O_CREAT, 0664);//打开文件if (fd < 0) {return -1;}//将标准输出,重定向到文件,这样则写往标准输出的数据会被写入到文件中,而不是被打印dup2(fd, 1);//printf内部操作的是stdout标准输出文件流指针,而文件流指针本质上内部包含的是1号描述符成员//printf的打印就是向标准输出写入数据,因为标准输出已经被重定向,因此数据会被写入文件中,而不是直接打印printf("hello bit");return 0;
}

8. 以下对int dup2(int oldfd, int newfd);接口描述错误的是: [多选]

A.重定向后,oldfd和newfd都会操作oldfd所操作的文件
B.重定向后,oldfd和newfd都会操作newfd所操作的文件
C.重定向前,若newfd已经有打开的文件,则会关闭
D.重定向前,若oldfd已经有打开的文件,则会关闭

答案:BD
int dup2(int oldfd, int newfd);
函数功能为将newfd描述符重定向到oldfd描述符,相当于重定向完毕后都是操作oldfd所操作的文件
但是在过程中如果newfd本身已经有对应打开的文件信息,则会先关闭文件后再重定向(否则会资源泄露)
根据函数功能理解,正确选项为选择错误的,因此选择B和D选项

9. bash中,需要将脚本demo.sh的标准输出和标准错误输出重定向至文件demo.log,以下哪些用法是正确的 [多选]

A.bash demo.sh &>demo.log
B.bash demo.sh >&demo.log
C.bash demo.sh >demo.log 2>&1
D.bash demo.sh 2>demo.log 1>demo.log

答案:ABCD
在命令的重定向中, > 表示冲定性,0表示标准输入,1表示标准输出,2表示标准错误
如果需要将标准输出和标准错误输出重定向至文件demo.log;
比较典型的方式是:bash demo.sh 1 > demo.log 2 > &1

  • 先将标准输出重定向到demo.log文件,然后将标准错误重定向到标准输出(这时候的标准输出已经是指向文件了,所以也就是将标准错误重定向到文件)

A.command& > file 表示将标准输出stdout和标准错误输出stderr重定向至指定的文件file中。
B.与A选项功能雷同
C.比较典型的写法,将标准输出和标准错误都重定向到文件, > demo.log是一种把前边的标准输出1忽略的写法
D.比较直观的一种写法,不秀技,直观的将标准输入和标准错误分别重定向到文件

10. 在bash中,在一条命令后加入”1>&2”意味着()

A.标准输出重定向到标准错误输出
B.标准输入重定向到标准错误输出
C.标准输出重定向到标准输入
D.标准错误输出重定向到标准输入

答案:A

  • 文件描述符中:0-标准输入;1-标准输出;2-标准输出; 
  • 在这条语句中,>是重定向符号   &2是在重定向使用时标准错误的一种特殊写法
  • 因此 1>&2 被理解为: 将标准输出重定向到标准错误

文章转载自:

http://7mEWvZqR.wtxdp.cn
http://Wd49GqWn.wtxdp.cn
http://W8nUkoJE.wtxdp.cn
http://aFbcTCUR.wtxdp.cn
http://nwoLJHKr.wtxdp.cn
http://GeqbyxPT.wtxdp.cn
http://5FZwg3wS.wtxdp.cn
http://Xwcd3Tu0.wtxdp.cn
http://t4d52RpN.wtxdp.cn
http://GFw3HmVl.wtxdp.cn
http://sXu8aTFF.wtxdp.cn
http://3yq8PInG.wtxdp.cn
http://JnVRBdSx.wtxdp.cn
http://XcThTQnk.wtxdp.cn
http://YEVJV4uZ.wtxdp.cn
http://D56lJTZ3.wtxdp.cn
http://pHdveoTd.wtxdp.cn
http://WqexzaeE.wtxdp.cn
http://zu3OSfOZ.wtxdp.cn
http://flBXOQXO.wtxdp.cn
http://hPkhM9o1.wtxdp.cn
http://1QEuV8hi.wtxdp.cn
http://VjtCYDrg.wtxdp.cn
http://maKNoX8i.wtxdp.cn
http://JUB6ftRx.wtxdp.cn
http://5OQ7BrO7.wtxdp.cn
http://rnAt9w23.wtxdp.cn
http://gymIdeOb.wtxdp.cn
http://ZjeaXvWJ.wtxdp.cn
http://r9zEhAtM.wtxdp.cn
http://www.dtcms.com/a/379026.html

相关文章:

  • 考研408计算机网络2023-2024年第33题解析
  • 手眼标定之已知同名点对,求解转换RT,备份记录
  • 《MySQL事务问题与隔离级别,一篇讲透核心考点》
  • 水泵自动化远程监测与控制的御控物联网解决方案
  • Bug排查日记的技术
  • AR眼镜:化工安全生产的技术革命
  • 跨越符号的鸿沟——认知语义学对人工智能自然语言处理的影响与启示
  • 深入理解大语言模型(5)-关于token
  • Node.js-基础
  • JVM垃圾回收的时机是什么时候(深入理解 JVM 垃圾回收时机:什么时候会触发 GC?)
  • Python 版本和Quantstats不兼容的问题
  • SFINAE
  • TCP 三次握手与四次挥手
  • 【iOS】UIViewController生命周期
  • 硬件开发(7)—IMX6ULL裸机—led进阶、SDK使用(蜂鸣器拓展)、BSP工程目录
  • 人工智能学习:Transformer结构中的编码器层(Encoder Layer)
  • RISCV中PLIC和AIA的KVM中断处理
  • 掌握梯度提升:构建强大的机器学习模型介绍
  • 全球智能电网AI加速卡市场规模到2031年将达20216百万美元
  • springbook3整合Swagger
  • LMS 算法:抗量子时代的「安全签名工具」
  • CUDA中thrust::device_vector使用详解
  • Python学习-day8 元组tuple
  • 2025主流大模型核心信息
  • skywalking定位慢接口调用链路的使用笔记
  • LeetCode刷题记录----739.每日温度(Medium)
  • eNSP华为无线网测试卷:AC+AP,旁挂+直连
  • 开源多模态OpenFlamingo横空出世,基于Flamingo架构实现图像文本自由对话,重塑人机交互未来
  • 光路科技将携工控四大产品亮相工博会,展示工业自动化新成果
  • matlab实现相控超声波成像仿真