Linux 基础IO与系统IO - 实践
从本章开始,我们逐渐导入文件系统的知识。要讲文件系统,文件与IO的知识就不得不讲,本章我们将从文件的认识和语言级别、系统级别的读写接口入手,深入了解基础IO与系统IO。
一.基础IO
1、文件认识
首先我们要明确:
文件=属性+内容
文件的读写实际上是进程对文件的操作
访问文件,首先要打开文件。执行完了open函数并成功,才算打开成功,那么是谁打开的文件?进程打开的文件,对文件的操作也就是进程对文件的操作。操作系统需要给用户提供系统级的文件操作系统调用。
系统中一定会有大量被打开的文件,各自与各自的(甚至多个)进程相关,操作系统自然就需要对这些被打开的文件管理:先描述,在组织。
宏观上,文件需要分两大类:“内存级”(被打开)的文件和磁盘级文件。下面我们慢慢进行展开。
2、语言层的IO
1.在这里我们用C语言的IO接口做演示,C语言的IO方式多种多样,这里我们就用最简单的文件打开、插入文字、关闭文件为例子。
1 #include2 #include34 int main(){5 FILE *fp=fopen("log.txt","w");67 if(fp==NULL){8 perror("fopen");9 return 1;10 }11 //我们用文本写入方式12 const char *msg="hello linux:";13 int cnt=1;14 while(cnt<=5){15 char buffer[1024];16 snprintf(buffer,sizeof(buffer),"%s%d",msg,cnt++);17 fwrite(buffer,strlen(buffer),1,fp);18 }1920 fclose(fp);21 return 0;2223 }
在这里我们调用C语言的fopen打开文件,若不存在则创建,写入方式打开。
创建一个字符数组并写入想要的数据,然后使用snprintf格式化输入进buffer数组中以便文本写入。
然后将buffer的内容写入文件log.txt
运行效果如下:
wujiahao@VM-12-14-ubuntu:~/file_test$ ./myfile
wujiahao@VM-12-14-ubuntu:~/file_test$ ls
log.txt Makefile myfile myfile.c
wujiahao@VM-12-14-ubuntu:~/file_test$ cat log.txt
hello linux:1hello linux:2hello linux:3hello linux:4hello linux:5wujiahao
2.我们还可以用C语言的接口fread实现简单的cat指令。为了能读取任意文件的内容,我们需要在程序的main函数中添加命令行参数argv和argc来接受外界(bash)的参数传递。
在此之前我们需要强调以下fread函数的参数和返回值。当fread执行成功时,会返回读取的字符传长度。
SYNOPSIS#include size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);
RETURN VALUEOn success, fread() and fwrite() return the number of items read or written. This number equals the number of bytes transferred only when size is 1. If an error occurs, or the end of the file isreached, the return value is a short item count (or zero).
源码如下:
1 #include2 #include34 int main(int argc,char *argv[]){5 if(argc!=2){6 printf("useage error!\n");7 return 1;8 }9 FILE *fp=fopen(argv[1],"r");10 if(!fp)11 {12 printf("fopen error!\n");13 return 2;14 }1516 char buffer[1024];17 while(1){18 int s=fread(buffer,1,sizeof(buffer),fp);19 if(s>0){20 buffer[s]=0;21 printf("%s",buffer);22 }23 if(feof(fp)){24 break;25 }26 }2728 fclose(fp);29 return 0;3031 }
运行结果:
wujiahao@VM-12-14-ubuntu:~/file_test$ ./myfile
useage error!
wujiahao@VM-12-14-ubuntu:~/file_test$ ./myfile Makefile
myfile:myfile.cg++ -o $@ $^ -std=c++11 #-std=c99
.PHONY:clean
clean:rm -f myfile
3.标准输入输出流
在之前语言的学习中我们就清楚,C语言在程序启动前都会默认打开三个输入输出流:
stdin:标准输入,键盘文件
stdout:标准输出,显示器文件
stderr:标准错误,显示器文件
除此之外,我们使用的printf函数默认向文件描述符为1的标准输出流打印数据,而fprintf函数可以指定其他输出流。我们可以先看看标准输入输出流的类型——是FILE *。而我们上面打开的文件指针fp等等也是这个类型。
SYNOPSIS#include extern FILE *stdin;extern FILE *stdout;extern FILE *stderr;
那么为什么要自动打开这三个输入输出流呢?
程序是为了处理数据,这些数据流的打开是给程序提供默认的数据源
以读w方式打开文件,文件会先被清空。拿重定向为例,我们第一次输入aaa到文件log.txt,第二次再写入bbb时会被覆盖,原因是会先把文件清空再进行写入。
若把打开方式设置为a的话,就是追加写入——从文件的读写位置末尾开始写。
1 #include2 #include34 int main(){5 FILE *fp=fopen("log.txt","a");6 if(fp==NULL){7 printf("fopen error");8 return 1;9 }1011 const char* msg="hello world\n";12 fprintf(fp,"%s",msg);1314 fclose(fp);1516 return 0;1718 }
可以发现,正常在文件末尾追加内容。
wujiahao@VM-12-14-ubuntu:~/file_test$ ./myfile
wujiahao@VM-12-14-ubuntu:~/file_test$ cat log.txt
hello linux:1hello linux:2hello linux:3hello linux:4hello linux:5hello world
wujiahao@VM-12-14-ubuntu:~/file_test$ ./myfile
wujiahao@VM-12-14-ubuntu:~/file_test$ ./myfile
wujiahao@VM-12-14-ubuntu:~/file_test$ ./myfile
wujiahao@VM-12-14-ubuntu:~/file_test$ cat log.txt
hello linux:1hello linux:2hello linux:3hello linux:4hello linux:5hello world
hello world
hello world
hello world
追加重定向的操作与此类似。
wujiahao@VM-12-14-ubuntu:~/file_test$ echo "test">>log.txt
wujiahao@VM-12-14-ubuntu:~/file_test$ echo "test">>log.txt
wujiahao@VM-12-14-ubuntu:~/file_test$ echo "test">>log.txt
wujiahao@VM-12-14-ubuntu:~/file_test$ cat log.txt
hello linux:1hello linux:2hello linux:3hello linux:4hello linux:5hello world
hello world
hello world
hello world
test
test
test
3、系统级的IO
讲完C语言提供的IO接口,我们来了解一下文件和系统级的IO接口。
我们可以把文件想象成一个一维数组,它的下标就是读写位置,每写进去一个内容就会向后移动,因此我们一写完就读会发现读取失败,是因为读写位置此时为空,我们需要对读写位置进行操作,用fseek系列函数
SYNOPSIS#include int fseek(FILE *stream, long offset, int whence);long ftell(FILE *stream);void rewind(FILE *stream);int fgetpos(FILE *stream, fpos_t *pos);int fsetpos(FILE *stream, const fpos_t *pos);
写字符串时不要把\0写进去,C语言中代表字符串结尾,但是系统与语言没有必然关系,输入\0会输出^@乱码。
1.open系统调用
SYNOPSIS#include #include #include int open(const char *pathname, int flags);int open(const char *pathname, int flags, mode_t mode);
open系统调用第一个参数就是打开的文件路径,第二个参数是标志位,第三个参数是按特定权限打开。
文件路径不必多说,我们需要在标志位上费一些笔墨讲解。
首先看看标志位是什么。
O_CLOEXEC, O_CREAT, O_DIRECTORY, O_EXCL, O_NOCTTY, O_NOFOLLOW, O_TMP‐ FILE, and O_TRUNC.
这里的标志位实际上类似于一些32位二进制数的宏,这些宏的意义其实就是C语言中我们说的打开方式。之所以要这样设计,是为了进行二进制传参。
操作系统在传参时是一个保守且谨慎的的行为,在open一个函数传入过多的参数可能会造成不必要的麻烦,而二进制传参就能很好的避免这一问题。实际上,open在接受flag时会进行按位或的操作来接受多个参数的传入,如下演示:
1 #include2 #include34 #define ONE_FLAG (1<<0)5 #define TWO_FLAG (1<<1)6 #define THREE_FLAG (1<<2)7 #define FOUR_FLAG (1<<2)8910 void Print(int flags){11 if(flags & ONE_FLAG){12 printf("one!\n");13 }14 if(flags &TWO_FLAG){1516 printf("two!\n");17 }18 if(flags &THREE_FLAG){1921 printf("three!\n");22 }23 if(flags & FOUR_FLAG){2427 printf("four!\n");28 }29 }3031 int main(){32 Print(ONE_FLAG);33 Print(ONE_FLAG|TWO_FLAG|THREE_FLAG|FOUR_FLAG);34 return 0;35 }
结果如下
wujiahao@VM-12-14-ubuntu:~/file_test$ ./myfile
one!
one!
two!
three!
four!
大致了解open的参数之后,我们可以来试着用一下它。
1 #include 2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 89 int main(){10 int fd=open("log.txt",O_CREAT|O_WRONLY,0666);11 if(fd<0)12 {13 perror("open");14 return 1;15 }1617 return 0;18 }
那问题来了,为什么other的权限还是664?因为我们系统中存在umask影响。如果设置权限时不想受系统umask影响,我们在代码中设置当前文件的掩码为0即可。
-rw-rw-r-- 1 wujiahao wujiahao 0 Sep 28 20:21 log.txt
关闭文件时我们需要close,传入open的返回值fd。
在这里我们现将问题保留,就是open的返回值fd。
2.write系统调用
1.write系统调用的参数比较简单,传入打开的文件描述符fd,要写入的字符数组,以及写入的长度。写入成功时返回写入的长度。
SYNOPSIS#include ssize_t write(int fd, const void *buf, size_t count);
int main(){10 int fd=open("log.txt",O_CREAT|O_WRONLY,0666);11 if(fd<0)12 {13 perror("open");14 return 1;15 }16 printf("fd:%d\n",fd);1718 const char *msg="hello!\n";19 int cnt=5;20 while(cnt--){21 write(fd,msg,strlen(msg));22 }2324 close(fd);2526 return 0;27 }
我们查看log,可以看到成功写入。
wujiahao@VM-12-14-ubuntu:~/file_test$ ./myfile
fd:3
wujiahao@VM-12-14-ubuntu:~/file_test$ cat log.txt
hello!
hello!
hello!
hello!
hello!
奇怪的是,我们在写一行,并且写入的消息不带反斜杠,会发现奇怪的现象:
const char *msg="its new";
int cnt=1;
文件中是覆盖写,而不是清空写。
wujiahao@VM-12-14-ubuntu:~/file_test$ cat log.txt
hello!
hello!
hello!
hello!
hello!
wujiahao@VM-12-14-ubuntu:~/file_test$ vim myfile.c
wujiahao@VM-12-14-ubuntu:~/file_test$ make
g++ -o myfile myfile.c -std=c++11 #-std=c99
wujiahao@VM-12-14-ubuntu:~/file_test$ ./myfile
fd:3
wujiahao@VM-12-14-ubuntu:~/file_test$ cat log.txt
its newhello!
hello!
hello!
hello!
那是因为在系统IO中,我们open传参时仅仅是创建并且只读,我们只有指定了清空写时他才会清空写,不要把系统的概念和C语言的概念混淆。open的清空,选项为TRUNC。
如果我们要追加,就不能选TRUNC,因为append是从末尾追加,清空是从开头写。
基础IO与系统IO:C语言的fopen在选择w或者a选项时,会对应转换成系统调用的选项。
2.文本写入vs二进制写入
我们往文件中写一个整数。
int cnt=1;20 int a=12345;21 while(cnt--){22 write(fd,&a,sizeof(a);23 }
结果输出一个乱码?
这种写入是一种二进制写入,文件大小也变成4字节。
-rw-rw-r-- 1 wujiahao wujiahao 4 Sep 28 20:39 log.txt
那如果我们把整数a用格式化输入snprintf处理,然后再将一个字符串数组写入文件:
19 int cnt=1;20 int a=12345;21 while(cnt--){22 char buffer[16];23 snprintf(buffer,sizeof(buffer),"%d",a);24 write(fd,buffer,strlen(buffer));25 }
这时的输出内容就正常了。
♦♦现象揭秘:
作为操作系统,根本不关心用户是二进制写入还是文本写入(write写入的类型都是void*),所谓的文本写入和二进制写入是一个语言层的概念(也就是用户的需求),所以c语言中就有了文本写入函数fputs和二进制写入fwrite等对系统调用不同程度封装的接口。
格式化是在干什么?将二进制的数据转换成文本,也是语言层的工作。
我们不用太关注二进制或文本写入,对于显示器这种设备被称为字符设备的原因,那就是它基本是文本输出的。
3.read系统调用
对于read,传参也同样需要传入文件描述符fd。
SYNOPSIS#include ssize_t read(int fd, void *buf, size_t count);
从指定文件描述符fd读,因为读文件新建没有意义,所以我们这里只需要传一个选项可读即可。
1 #include 2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 89 int main(){10 int fd=open("log.txt",O_RDONLY);11 if(fd<0)12 {13 perror("open");14 return 1;15 }16 printf("fd:%d\n",fd);1718 while(1){19 char buffer[64];20 int n=read(fd,buffer,sizeof(buffer)-1);21 if(n>0){22 buffer[n]=0;23 printf("%s",buffer);24 }25 else if(n==0)26 {27 break;28 }29 }
结果如下:我们将log的内容读到字符数组buffer中,并回显到屏幕上。
wujiahao@VM-12-14-ubuntu:~/file_test$ ./myfile
fd:3
12345
和上面的道理类似,读出来是什么格式系统不关心,但用户需要文本输出的话就需要自己处理。
可以说Linux系统中只有open,write,read,close这四个读写接口,语言层都是对他们进行不同程度封装的结果
4.文件描述符fd
现在,来详细说说,open的返回值fd是什么?
fd叫文件描述符,他是一个整数。打开多个文件试试:
1 #include 2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 89 int main(){10 umask(0);11 int fd1 = open("log1.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);12 int fd2 = open("log2.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);13 int fd3 = open("log3.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);14 int fd4 = open("log4.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);15 if(fd1 < 0)exit(1);16 if(fd2 < 0)exit(1);17 if(fd3 < 0)exit(1);18 if(fd4 < 0)exit(1);1920 printf("fd1: %d\n", fd1);21 printf("fd2: %d\n", fd2);22 printf("fd3: %d\n", fd3);23 printf("fd4: %d\n", fd4);2425 close(fd1);26 close(fd2);27 close(fd3);28 close(fd4);
结果:
wujiahao@VM-12-14-ubuntu:~/file_test$ ./myfile
fd1: 3
fd2: 4
fd3: 5
fd4: 6
可以说,打开文件成功时的返回值都是大于0的。那么大家有没有好奇,0,1,2都是些啥?
没错,就是操作系统默认为我们打开的stdin,stdout,stderr:标准输入输出流。
我们可以预测的是,fopen的返回值FILE中一定封装了文件描述符fd,OS中只认文件描述符fd,C语言的东西一概不认。而这些标准输入输出流 中FILE封装的fd一定为0,1,2。我们可以简单验证:
printf("stdin: %d\n", stdin->_fileno);20 printf("stdout: %d\n",stdout->_fileno);21 printf("stderr: %d\n", stderr->_fileno);
结果:
stdin: 0
stdout: 1
stderr: 2
fd1: 3
fd2: 4
fd3: 5
fd4: 6
那么文件描述符fd是什么?
什么样的数据,会呈现0123456的样式?
没错,就是数组。
操作系统在打开一个文件时,会创建一个struct file:
那么打开多个文件,就会有:
struct file会对应链接一个文件缓冲区,那么它对应的文件内容会被加载到缓冲区,文件属性会被初始化struct file。当前进程通过文件描述符拿到文件,也会对应创建一个文件描述符表struct file_struct,包含存放fd的数组以及当前打开的文件指针数组,那么当前进程为了访问对应的文件,就需要拿到文件描述符标的数组下标进行访问。
当用户层使用open时,就创建一个struct file,并在文件描述符标的指针数组中找一个没使用过的下标,把struct file的地址填入。
用户调用read时,创建一个buffer来读数据,操作系统拿着fd对数组进行索引找到对应文件,将struct file的文件缓冲区,再把缓冲区内容拷贝到用户创建的buffer中,read本质是内核到用户空间的数据拷贝。
那么增改文件,对于冯氏结构我们知道,CPU无法直接对磁盘进行操作,所以需要先把磁盘文件内容加载到缓冲区,然后再内存中做修改,最后在写回磁盘。
也就是说,对文件内容进行任何操作,都需要把文件从磁盘拷贝到内存(加载到内核对应的文件缓冲区)。
5.重定向的原理
文件描述符的分配原则:当我们新打开一个文件时,会在表中分配最小的且没有被使用的文件描述符fd。
我们关闭fd1,新打开文件log.txt就会被默认分配到较小下标的1处。而我们执行printf时默认向stdout(fd=1)打印,而此时fd1指向的是log,所以此时打印会写到log中。注意这里后面没有对log关闭。
这就是重定向原理——更改文件描述符表的指针指向,数组下标不变。
dup2系统调用
我们上面对files_struct的指针指向修改属于对内核数据结构的修改,所以我们需要dup2系统调用做这件事。
这里需要注意的是dup2的用法。dup2会使newfd生成一份oldfd的拷贝,也就是说我们如果要把fd1的默认指向从标准输出指向我们自己的myfile,应该传参dup2(fd,1)。
本来要打印到显示器中的内容,打印到了log中。
如果我们想实现任意文件的输入重定向,就在main的参数中传入argv,让外部调用时的输入直接从指定的文件读取
重定向:打开文件的方式+dup2.
明确了重定向的原理,我们可以完善上章的简单shell重定向功能。
shell重定向
先通过宏定义重定向方式和文件名。
// 4. 关于重定向,我们关心的内容
#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3
int redir = NONE_REDIR;
std::string filename;
这次我们在解析命令前,先要解析是不是重定向操作,需要空格消除方法。
void TrimSpace(char cmd[], int &end)
{while(isspace(cmd[end])){end++;}
}
然后判定重定向方式。
void RedirCheck(char cmd[])
{redir = NONE_REDIR;filename.clear();int start = 0;int end = strlen(cmd)-1;//"ls -a -l >> file.txt" > >> <while(end > start){if(cmd[end] == '<'){cmd[end++] = 0;TrimSpace(cmd, end);redir = INPUT_REDIR;filename = cmd+end;break;}else if(cmd[end] == '>'){if(cmd[end-1] == '>'){//>>cmd[end-1] = 0;redir = APPEND_REDIR;}else{//>redir = OUTPUT_REDIR;}cmd[end++] = 0;TrimSpace(cmd, end);filename = cmd+end;break;}else{end--;}}
}
重定向处理:让子进程进行重定向操作。
int Execute()
{pid_t id = fork();if(id == 0){int fd = -1;// 子进程检测重定向情况if(redir == INPUT_REDIR){fd = open(filename.c_str(), O_RDONLY);if(fd < 0) exit(1);dup2(fd,0);close(fd);}else if(redir == OUTPUT_REDIR){fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666);if(fd < 0) exit(2);dup2(fd, 1);close(fd);}else if(redir == APPEND_REDIR){fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666);if(fd < 0) exit(2);dup2(fd, 1);close(fd);}else{}// 进程替换,会影响重定向的结果吗?不影响//childexecvp(g_argv[0], g_argv);exit(1);}int status = 0;// fatherpid_t rid = waitpid(id, &status, 0);if(rid > 0){lastcode = WEXITSTATUS(status);}return 0;
}
此时主函数main的逻辑如下:
InitEnv();while(true){// 1. 输出命令行提示符PrintCommandPrompt();// 2. 获取用户输入的命令char commandline[COMMAND_SIZE];if(!GetCommandLine(commandline, sizeof(commandline)))continue;// 3. 重定向分析 "ls -a -l > file.txt" -> "ls -a -l" "file.txt" -> 判定重定向方式RedirCheck(commandline);
// printf("redir: %d, filename: %s\n", redir, filename.c_str());// 4. 命令行分析 "ls -a -l" -> "ls" "-a" "-l"if(!CommandParse(commandline))continue;//PrintArgv();// 检测别名// 5. 检测并处理内键命令if(CheckAndExecBuiltin())continue;// 6. 执行命令Execute();}
进程替换会影响重定向吗?毫不影响。因为程序替换是进程和进程地址空间,页表,物理内存的数据代码交互,而重定向改变的是内核数据结构的东西,我们后续子进程的操作都是在重定向后的文件中进行操作的
如果我们重定向的对象是网络,是不是就可以理解为进行网络的输入输出了?
在做内建命令重定向的处理,我们可以打开另一个临时文件保存标准输入输出流的指针,先dup2让他执行别的操作,然后再dup2回来即可,比较类似我们之前的swap操作。
tips:一个文件可以被多个进程打开,采用引用计数管理。
6.可移植性
那么我们之前学习的文件操作,FILE返回值封装了文件描述符,接口封装了系统调用。
我们来谈谈封装。为什么封装?
上层写的代码能在不同平台下能运行是由库屏蔽细节,实现可移植性。把平台之间的差异封装到库中。
C++等其他语言的接口如何呢?每一门语言对IO流的操作都各不相同,不管他们上层封装再怎么变化,底层都只认文件描述符fd。
要谈论可移植性,首先要说什么是不可移植性。是平台的原因。操作系统不一样,系统调用接口不一样,那么平台之间的可移植性就比较差。语言为了增加自己的可移植性,所以对不同平台都做了封装实现。
那为什么语言要增加自己的可移植性?平台背后是用户,语言增加可移植性是为了让更多用户用,有着他们自己的商业驱动。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/922185.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!