基础IO_系统文件IO | 重定向【Linux】
文章目录
- 一、 理解"文件"
- 1、狭义理解
- 2、广义理解
- 3、文件操作的归类认知
- 4、系统角度
- 二、回顾C文件接口
- 1、hello.c打开文件
- 2、hello.c写文件
- 3、hello.c读文件
- 4、输出信息到显示器,你有哪些方法
- 5、stdin & stdout & stderr
- 6、打开文件的方式
- 三、系统文件I/O
- 1、一种传递标志位的方法
- 2、hello.c 写文件:
- 3、hello.c读文件
- 4、接口介绍
- 5、open函数返回值
- 6、文件描述符fd
- 6.1、0 & 1 & 2
- 6.2、文件描述符的分配规则
- 6.3、重定向
- 6.4、使用 dup2 系统调用
- 6.5、在minishell中添加重定向功能
一、 理解"文件"
1、狭义理解
- 文件在磁盘里
- 磁盘是永久性存储介质,因此文件在磁盘上的存储是永久性的
- 磁盘是外设(即是输出设备也是输入设备)
- 磁盘上的文件 本质是对文件的所有操作,都是对外设的输入和输出 简称 IO
2、广义理解
- Linux 下一切皆文件(键盘、显示器、网卡、磁盘…… 这些都是抽象化的过程)
3、文件操作的归类认知
- 对于 0KB 的空文件是占用磁盘空间的
- 文件是文件属性(元数据)和文件内容的集合(文件 = 属性(元数据)+ 内容)
- 所有的文件操作本质是文件内容操作和文件属性操作
4、系统角度
- 对文件的操作本质是进程对文件的操作
- 磁盘的管理者是操作系统
- 文件的读写本质不是通过 C 语言 / C++ 的库函数来操作的(这些库函数只是为用户提供方便),而是通过文件相关的系统调用接口来实现的
二、回顾C文件接口
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文件在哪个路径下?
xz@xzlinux:~$ ps ajx | head -1;ps ajx | grep catmePPID PID PGID SID TTY TPGID STAT UID TIME COMMAND336450 336595 336595 336435 pts/0 336595 R+ 1000 0:02 ./catme336584 336599 336598 336569 pts/1 336598 S+ 1000 0:00 grep --color=auto catme
xz@xzlinux:~$ ls /proc/336595 -l
total 0
......
-r--r--r-- 1 xz xz 0 May 8 15:34 cpuset
lrwxrwxrwx 1 xz xz 0 May 8 15:34 cwd -> /home/xz/z/IOleran
-r-------- 1 xz xz 0 May 8 15:34 environ
lrwxrwxrwx 1 xz xz 0 May 8 15:34 exe -> /home/xz/z/IOleran/catme
dr-x------ 2 xz xz 4 May 8 15:34 fd
......
其中:
- cwd:指向当前进程运行目录的一个符号链接。
- exe:指向启动当前进程的可执行文件(完整路径)的符号链接。
打开文件,本质是进程打开,所以,进程知道自己在哪里,即便文件不带路径,进程也知道。由此OS就能知道要创建的文件放在哪里。
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;
}
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 buff[1024];const char *msg = "hello bit!\n";while(1){ssize_t s = fread(buff,1,strlen(msg),fp);if(s > 0){buff[s] = 0;printf("%s",buff);}if(feof(fp)){break;}}fclose(fp);return 0;
}
稍作修改,实现简单cat命令:
#include <stdio.h>
#include <string.h>//简单实现cat命令
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;
}
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;
}
5、stdin & stdout & stderr
#include <stdio.h>extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;
6、打开文件的方式
r Open text file for reading. The stream is positioned at the beginning of thefile.r+ Open for reading and writing. The stream is positioned at the beginning of thefile.w Truncate file to zero length or create text file for writing. The stream is po‐sitioned at the beginning of the file.w+ Open for reading and writing. The file is created if it does not exist, other‐wise 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 notexist. 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 ifit does not exist. Output is always appended to the end of the file. POSIX issilent on what the initial read position is when using this mode. For glibc, theinitial file position for reading is at the beginning of the file, but for An‐droid/BSD/MacOS, the initial file position for reading is at the end of the file.
如上,是文件相关操作。还有 fseek ftell rewind
的函数,在C部分已经有所涉猎。
三、系统文件I/O
打开文件的方式不仅仅是fopen,ifstream
等流式,语言层的方案,其实系统才是打开文件最底层的方案。不过,在学习系统文件IO之前,先要了解下如何给函数传递标志位,该方法在系统文件IO接口中会使用到:
1、一种传递标志位的方法
#include <stdio.h>
#include <string.h>#define ONE 0001 //0000 0001
#define TWO 0002 //0000 0001
#define THREE 0004 //0000 0001void 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 | THREE);func(ONE | THREE | TWO);return 0;
}
操作文件,除了上面的C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问, 先来直接以系统代码的形式,实现和上面一模一样的代码:
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 xz!\n";int len = strlen(msg);while (count--) {write(fd, msg, len);//fd: 后面讲, msg:缓冲区首地址。//len: 本次读取,期望写入多少个字节的数据。 //返回值:实际写了多少字节数据} close(fd);return 0;
}
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_tssize_t s = read(fd, buf, strlen(msg));//类比writeif (s > 0) {printf("%s", buf);}else {break;}} close(fd);return 0;
}
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文件相关接口。
5、open函数返回值
在认识返回值之前,先来认识一下两个概念: 系统调用
和 库函数
- 上面的
fopen fclose fread fwrite
都是C标准库当中的函数,我们称之为库函数
(libc)。 - 而
open close read write lseek
都属于系统提供的接口,称之为系统调用接口 - 回忆一下我们讲操作系统概念时,画的一张图
系统调用接口和库函数的关系,一目了然。
所以,可以认为, f#
系列的函数,都是对系统调用的封装,方便二次开发。
6、文件描述符fd
- 通过对open函数的学习,我们知道了文件描述符就是一个小整数
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;
}
而现在知道,文件描述符就是从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
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>
#inc
lude <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数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
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。这种现象叫做输出重定向。常见的重定向有: > , >> , <
那重定向的本质是什么呢?
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的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成输出重定向。
6.5、在minishell中添加重定向功能
重定向myshell—https://gitee.com/xiaozhi