🌈个人主页:秦jh__https://blog.csdn.net/qinjh_?spm=1010.2135.3001.5343
🔥 系列专栏:https://blog.csdn.net/qinjh_/category_12625432.html
目录
前言
C文件IO相关操作
系统文件I/O
open
open函数返回值
文件描述符fd
read、stat
文件描述符的分配规则
重定向
使用 dup2 系统调用
缓冲区
缓冲区的刷新策略
自主shell补充
封装简单库
stderr
前言
💬 hello! 各位铁子们大家好哇。
今日更新了Linux基础IO的内容
🎉 欢迎大家关注🔍点赞👍收藏⭐️留言📝
C文件IO相关操作
1 #include<stdio.h> 2 3 int main() 4 { 5 FILE* fp=fopen("log.txt","w"); 6 if(NULL==fp) 7 { 8 perror("fopen"); 9 return 1; 10 } 11 fclose(fp); 12 return 0; 13 }
执行上面代码后,创建了log.txt。运行代码时,进程就跑起来了,此时进程所在的路径就是当前进程的工作路径,所以在此创建文件。打开文件的本质其实是进程打开文件。
文件存在,但没有被打开时存在于磁盘中。
进程可以打开很多文件,系统中可以有很多进程。
文件=内容+属性
创建文件时, 没有内容,大小显示的是0kb,但它还是占一定空间的,因为他有各种属性。
系统文件I/O
open
pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
上面这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限 O_APPEND: 追加写
O_TRUNC: 如果文件已经存在,而且是个常规文件,以写的方式打开,传入这个选项后,他就会把文件清空。
返回值:
成功:新打开的文件描述符
失败:-1
mode_t理解:就是int类型,表示起始权限。
open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open。
运行上面代码,发现创建了log.txt,但是它的权限是乱码的。
我们加上666权限后,现在权限不是乱码了,但权限依旧不是666对应的权限,而是664。这是因为权限掩码会与你的设置的权限,进行位运算,结果就不一样了。系统默认权限掩码是0002
所以我们可以在打开文件前设置默认的权限掩码为0
此时权限就对应上了。注意:权限掩码按照就近原则,如果我们有设置默认权限掩码,就用我们设置的,如果没有,就会使用系统默认的。
上面是系统调用接口close和write。fd就是open的返回值。
我们把message里的内容换成abc,然后直接运行代码。发现之前的内容还在,旧的内容没被清空。
这是因为这里的open默认不存在就创建,存在就打开,默认不清空文件。
如果我们想像C语言fopen的“w”打开方式一样, 打开就清空文件,就需要再传一个选项O_TRUNC。
它表示 如果文件已经存在,而且是个常规文件,并以写的方式打开,传入这个选项后,他就会把文件清空。
O_APPEND就是追加的意思。
open函数返回值
在认识返回值之前,先来认识一下两个概念:系统调用和库函数
fopen fclose fread fwrite都是C标准库当中的函数,我们称之为库函数(libc)。
open close read write lseek 都属于系统提供的接口,称之为系统调用接口
可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。
文件描述符fd
文件描述符就是一个小整数
open的返回值fd是从3开始的。因为C语言默认会打开三个输入输出流,
标准输入stdin、
标准输出stdout、
标准错误stderr 。
我们可以用write配合文件描述符在显示器上打印。
文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件 。
Linux中一切皆文件,所以0,1,2可以代表键盘,显示器。
在OS内,系统在访问文件的时候,只认文件描述符fd。
FILE* 是C语言提供的结构体类型,里面封装着文件fd。
所有的C语言上的文件操作函数,本质都是对系统调用的封装。
FILE* 结构体中就封装着文件描述符fd。
学了系统调用,我们可以用系统调用接口,也可以用语言提供的文件方法。但还是推荐使用语言提供的方法。因为系统不同,系统调用的接口可能不一样。
我们打开的文件是当前xshell的终端。
系统中有一个proc目录,里面有很多蓝色的文件夹,它是由进程的pid来做的。
我们查看某个进程的文件夹。cwd就是当前进程的工作路径。exe指向当前可执行程序的二进制文件。里面还有一个目录fd。
进入fd目录,可以看到默认的文件描述符0、1、2是打开的。
打开的设备是dev目录下的pts/3。
/dev/pts/3 指的就是我打开的右边的界面,也就是终端,这个终端文件就叫/dev/pts/3。
运行后如上图,会在另一个终端上打印。
云服务器下,我们看到的显示器文件一般在 /dev/pts/目录下
如果再登录一个就多了一个新的文件。
read、stat
运行上面代码,结果如下图:
struct stat是一个内核结构体,可以直接用。stat的参数2是一个输出型参数,我们把参数传进去后,它会把参数填满然后再传出来。
运行后,我们就可以读取文件里的内容了,如下图:
read的参数1指读取的文件fd,参数2是将读取到的内容放到该缓冲区中,参数3是要读取的字节数。
read的返回值:>0 :读取到的字节数 =0:已经读取到文件末尾。
文件描述符的分配规则
因为文件描述符的0、1、2默认是打开的,所以这里结果是3。如果我们先把描述符0关了再打开新文件会怎样呢?
结果是0。
如果我们把2先关了 ,这时候结果就是2了。
从上面的结果可以得出结论,
文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
重定向
如果我们先把1关闭,发现结果什么也不打印。这是因为文件描述符1是标准输出流,关闭后,就不会在显示器打印了。
运行上面代码,发现什么也没打印,但确实创建了新的文件log.txt。打印该文件,发现内容写在了文件里面。
如果我们把fflush注释掉,发现log.txt创建出来了,但里面什么东西也没有。
log.txt存在磁盘中,当进程启动打开时,就会被加载到内存中。由于我们先关闭了文件描述符1,所以此时log.txt的文件描述符就是1。上层的printf和fprintf都是向stdout打印,而stdout的描述符是1,OS只认文件描述符,所以最终就向log,txt打印了内容。
重定向的本质:是在内核中改变文件描述符表特定下标的内容,与上层无关!
每个文件对象都有对应的内核文件缓冲区,我们写数据都是从上层通过文件描述符1,写到对应的文件缓冲区,然后OS再把内容刷新到磁盘的文件中。
stdin、stdout、stderr都是FILE*结构体,里面除了封装着fd,还有语言级别的文件缓冲区。所以我们通过printf/fprintf不是直接写到OS的内部的缓冲区,而是直接写到语言级别的缓冲区中,然后C语言再通过1号文件描述符把内容刷新到OS的内核文件缓冲区中。
所以fflush()里面是stdout,这是因为我们是刷新语言级别缓冲区的内容到OS的内核缓冲区中,内核缓冲区的内容由OS进行刷新。
由上可知,之所以注释掉fflush后,log.txt里面啥也没有,是因为内容在语言级别的缓冲区中,还没执行到return语句,冲刷内容到内核缓冲区中,log.txt就被关闭了。
如果我们把close也注释掉,结果如下:
return的时候,语言级别缓冲区的内容就被冲刷到内核文件缓冲区中,此时log.txt就有内容了。
使用 dup2 系统调用
dup2可以在底层帮我们做两个文件描述符对应的数组内容之间的值拷贝 。
本质是文件描述符下标对应内容的拷贝。
原本1号文件的内容指向显示器,3号文件内容指向log.txt。重定向的本质是将3号的内容拷贝给1号。所以1号就不会再指向显示器了,而是变成指向log.txt,所以后来往1号里写的内容都会变成往log.txt里写。
struct file里还存在一个引用计数,有几个指针指向就是几。如log.txt由1号和3号指向就是2,显示器就是0。
如果我们要对标准输出进行重定向,把往显示器打印的内容变成往log,txt打印,根据上面的参数解释,参数的填法应该是dup2(fd,1)。也就是把oldfd留下来,拷贝给newfd。
运行上面代码,发现不在显示器上打印,而是在log.txt里打印。
我们把选项换成O_APPEND,它就会进行追加了。所以>和>>的区别就是选项不同而已。
缓冲区
缓冲区就是一段内存空间。
缓冲区由C语言维护就叫语言级缓冲区,由OS维护就叫内核缓冲区。
缓冲区存在的意义:OS为语言考虑,语言为用户考虑。给上层提供高效的IO体验,间接提高整体效率。
缓冲区的刷新策略
- 立即刷新。(无缓存) fflush(stdout)、fsync(int fd)
- 行刷新。显示器
- 全缓冲。缓冲区写满才刷新。普通文件。
- 特殊情况:
- 进程退出,系统自动刷新。
- 强制刷新。
这个刷新策略在内核和用户级别的缓冲区都能用。这里介绍用户级别的。
运行上面代码,第一次在显示器上打印,第二次重定向到文件打印。发现打印的顺序不同。这是因为在显示器上打印是行刷新策略,write系统调用没有带缓冲区,就按语句顺序打印,所以第一次打印按顺序。库函数print和fprintf最后都是通过write系统调用刷新。
第二次是重定向到普通文件,此时刷新策略变成全缓冲,执行printf和fprintf语句时,内容都在缓冲区中,write直接输出,然后程序结束自动把缓冲区刷新,才打印出printf和fprintf。
直接运行跟上面的第一次一样,因为是行刷新,所以执行到fork()时,缓冲区没内容了。
如果重定向到普通文件,此时是全缓冲,printf和fprintf的内容都在语言级缓冲区中,write是直接写到内核缓冲区中,所以write打印在最前面且只打印一次。到了fork时,父子进程都有了语言及缓冲区的内容,所以程序结束时,父子进程的缓冲区的内容都被刷新,就打印两次printf和fprintf。
- 一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
- printf fprintf 库函数会自带缓冲区,当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。
- 而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后
- 但是进程退出之后,会统一刷新,写入文件当中。
- 但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的 一份数据,随即产生两份数据。
- write 没有变化,说明没有所谓的缓冲。
printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区, 都是用户级缓冲区。
自主shell补充
补充了重定向的功能
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 #include <errno.h> 5 #include <unistd.h> 6 #include <sys/types.h> 7 #include <sys/wait.h> 8 #include <ctype.h> 9 #include <sys/stat.h> 10 #include <fcntl.h> 11 12 #define SIZE 512 13 #define ZERO '/0' 14 #define SEP " " 15 #define NUM 32 16 #define SkipPath(p) do{ p += (strlen(p)-1); while(*p != '/') p--; }while(0) 17 #define SkipSpace(cmd, pos) do{/ 18 while(1){/ 19 if(isspace(cmd[pos]))/ 20 pos++;/ 21 else break;/ 22 }/ 23 }while(0) 24 25 26 // "ls -a -l -n > myfile.txt" 27 #define None_Redir 0 28 #define In_Redir 1 29 #define Out_Redir 2 30 #define App_Redir 3 31 32 int redir_type=None_Redir; 33 char* filename=NULL; 34 35 // 为了方便,我就直接定义了 36 char cwd[SIZE*2]; 37 char *gArgv[NUM]; 38 int lastcode = 0; 39 40 void Die() 41 { 42 exit(1); 43 } 44 45 const char *GetHome() 46 { 47 const char *home = getenv("HOME"); 48 if(home == NULL) return "/"; 49 return home; 50 } 51 52 const char *GetUserName() 53 { 54 const char *name = getenv("USER"); 55 if(name == NULL) return "None"; 56 return name; 57 } 58 const char *GetHostName() 59 { 60 const char *hostname = getenv("HOSTNAME"); 61 if(hostname == NULL) return "None"; 62 return hostname; 63 } 64 // 临时 65 const char *GetCwd() 66 { 67 const char *cwd = getenv("PWD"); 68 if(cwd == NULL) return "None"; 69 return cwd; 70 } 71 72 // commandline : output 73 void MakeCommandLineAndPrint() 74 { 75 char line[SIZE]; 76 const char *username = GetUserName(); 77 const char *hostname = GetHostName(); 78 const char *cwd = GetCwd(); 79 80 SkipPath(cwd); 81 snprintf(line, sizeof(line), "[%s@%s %s]> ", username, hostname, strlen(cwd) == 1 ? "/" : cwd+1); 82 printf("%s", line); 83 fflush(stdout); 84 } 85 86 int GetUserCommand(char command[], size_t n) 87 { 88 char *s = fgets(command, n, stdin); 89 if(s == NULL) return -1; 90 command[strlen(command)-1] = ZERO; 91 return strlen(command); 92 } 93 94 95 void SplitCommand(char command[], size_t n) 96 { 97 (void)n; 98 // "ls -a -l -n" -> "ls" "-a" "-l" "-n" 99 gArgv[0] = strtok(command, SEP);100 int index = 1;101 while((gArgv[index++] = strtok(NULL, SEP))); // done, 故意写成=,表示先赋值,在判断. 分割之后,strtok会返回NULL,刚好让gArgv最后一个元素是NULL, 并且while判断结束102 }103 104 void ExecuteCommand()105 {106 pid_t id = fork();107 if(id < 0) Die();108 else if(id == 0)109 {110 //重定向设置111 if(filename != NULL){112 if(redir_type == In_Redir)113 {114 int fd = open(filename, O_RDONLY);115 dup2(fd, 0);116 }117 else if(redir_type == Out_Redir)118 {119 int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);120 dup2(fd, 1);121 }122 else if(redir_type == App_Redir)123 {124 int fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);125 dup2(fd, 1);126 }127 else128 {}129 }130 131 132 // child 133 execvp(gArgv[0], gArgv);134 exit(errno);135 }136 else137 {138 // fahter139 int status = 0;140 pid_t rid = waitpid(id, &status, 0);141 if(rid > 0)142 {143 lastcode = WEXITSTATUS(status);144 if(lastcode != 0) printf("%s:%s:%d/n", gArgv[0], strerror(lastcode), lastcode);145 }146 }147 }148 149 void Cd()150 {151 const char *path = gArgv[1];152 if(path == NULL) path = GetHome();153 // path 一定存在154 chdir(path); //更改当前的工作路径155 156 // 刷新环境变量157 char temp[SIZE*2];//临时缓冲区158 getcwd(temp, sizeof(temp)); //得到当前进程的绝对路径 159 snprintf(cwd, sizeof(cwd), "PWD=%s", temp);//160 putenv(cwd); // 导入新的环境变量161 }162 163 int CheckBuildin()164 {165 int yes = 0;166 const char *enter_cmd = gArgv[0];167 if(strcmp(enter_cmd, "cd") == 0)168 {169 yes = 1;170 Cd();171 }172 else if(strcmp(enter_cmd, "echo") == 0 && strcmp(gArgv[1], "$?") == 0)173 {174 yes = 1; 175 printf("%d/n", lastcode);176 lastcode = 0;177 }178 return yes;179 }180 181 182 void CheckRedir(char cmd[])183 { 184 // > >> <185 // "ls -a -l -n > myfile.txt"186 int pos = 0;187 int end = strlen(cmd);188 189 while(pos < end)190 {191 if(cmd[pos] == '>')192 {193 if(cmd[pos+1] == '>')194 {195 cmd[pos++] = 0;196 pos++;197 redir_type = App_Redir;198 SkipSpace(cmd, pos);199 filename = cmd + pos;200 }201 else202 {203 cmd[pos++] = 0;204 redir_type = Out_Redir;205 SkipSpace(cmd, pos);206 filename = cmd + pos;207 }208 }209 else if(cmd[pos] == '<')210 {211 cmd[pos++] = 0;212 redir_type = In_Redir;213 SkipSpace(cmd, pos);214 filename = cmd + pos;215 }216 else217 {218 pos++;219 } 220 }221 }222 223 int main()224 {225 int quit = 0;226 while(!quit)227 {228 //0.重置229 redir_type=None_Redir;230 filename=NULL;231 // 1. 我们需要自己输出一个命令行232 MakeCommandLineAndPrint();233 234 // 2. 获取用户命令字符串235 char usercommand[SIZE];236 int n = GetUserCommand(usercommand, sizeof(usercommand));237 if(n <= 0) return 1;238 239 //2.1checkredir240 CheckRedir(usercommand);241 242 // 3. 命令行字符串分割. 243 SplitCommand(usercommand, sizeof(usercommand));244 245 // 4. 检测命令是否是内建命令246 n = CheckBuildin();247 if(n) continue;248 // 5. 执行命令249 ExecuteCommand();250 }251 return 0;252 }
封装简单库
mystdio.h
#pragma once#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 4struct _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);
mystdio.c
#include "mystdio.h"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);}
stderr
stdout和stderr分别对应文件描述符1和2,他们都指向显示器文件。>是标准输出重定向,只更改1号fd里面的内容,所以重定向后,1号的打印到了log,txt,而2号还是没变,依旧打印在显示器上。
直接运行代码,会全部打印在显示器上。我们可以重定向到不同文件,这样就可以将正确信息和错误信息分出来。这也是fd1,fd2的意义。上面是完整的重定向的写法。
如果我们想把1和2都重定向到同一个文件中,可以通过上面的写法实现。