【Linux】基础IO
需要云服务器等云产品来学习Linux的同学可以移步/–>腾讯云<–/官网,轻量型云服务器低至112元/年,优惠多多。(联系我有折扣哦)
文章目录
1.回顾C语言的文件操作
有一个笑话:把大象装冰箱需要几步?“打开冰箱门,把大象装进去,关闭冰箱门”。实际上,我们对文件的操作也是这样的:“打开文件,读写文件,关闭文件”。这里演示一下C语言对文件的操作。详细内容可以去博主的其他文章了解【C语言进阶】文件操作
1.1 打开和关闭
#include <stdio.h>
#define FILE_NAME "log.txt"
int main()
{
// fopen的返回值类型是FILE*,返回一个文件指针,打开失败返回NULL
FILE* fp = fopen(FILE_NAME, "r");//fopen传参数:1.文件名 2.打开方式:"w"只写, "r"只读, "a"追加, "w+"读写, "r+"读写, "a+"追加读写
if(fp == NULL)
{
perror("open");//打印失败原因
return 1;
}
else
{
printf("%s打开成功\n", FILE_NAME);
}
//文件打开了就需要关闭
int ret = fclose(fp);//fclose传参数:FILE*类型,要关闭的文件指针
//fclose的返回值类型int,如果关闭成功返回0,关闭失败返回EOF
if(ret == 0)
{
printf("%s关闭成功", FILE_NAME);
}
else
{
perror("关闭失败");//打印失败原因
return 2;
}
return 0;
}
1.2 文件读写
文件的读写是对应的,一个读对应着一个写,有以下几种读写方式
int fputc(int c, FILE *stream); // 向stream写入一个字符
int fgetc(FILE *stream); // 从stream读取一个字符
int fputs(const char *s, FILE *stream); // 向stream写入一个字符串
char *fgets(char *s, int size, FILE *stream); // 从stream读取一个字符串
int fprintf(FILE *stream, const char *format, ...); // 向stream中格式化写入
int fscanf(FILE *stream, const char *format, ...); // 从stream中格式化读取
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); // 向stream中以二进制的方式写入
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); // 从stream中以二进制的方式读取
1. 文件写入
2. 文件读取
2. 系统文件I/O
2.1 比特位传递选项
在C语言中,我们想要传递标记的话,一般使用一个整数来传递一个标记,但是我们知道一个int有32个比特位,所以实际上,我们可以通过比特位来传递选项,这是一个非常巧妙的设计:使用每一个比特位代表一个选项,1表示具有该选项,0表示不具有该选项,如果想要表达同时具有某一些选项,那就将这些选项对应的值按位或在一起
例如:
#include <stdio.h> //选项的宏定义,每个宏对应一个比特位 #define ONE (1 << 0) #define TWO (1 << 1) #define THREE (1 << 2) #define FOUR (1 << 3) void show(int flag) { if(flag & ONE) printf("ONE\n"); if(flag & TWO) printf("TWO\n"); if(flag & THREE) printf("THREE\n"); if(flag & FOUR) printf("FOUR\n"); } int main() { show(ONE); printf("-------------------\n"); show(TWO); printf("-------------------\n"); show(ONE | TWO); printf("-------------------\n"); show(ONE | TWO | THREE); printf("-------------------\n"); show(ONE | TWO | THREE | FOUR); return 0; }
2.2 打开和关闭
写在前面:对于任何一种语言来说,一定都提供了自己的文件操作接口,那么每一种语言的文件操作都是不同的,所以我们的学习成本很高。但是,所有的程序都是运行在操作系统上的,所以对于任何一种语言,他最终都会让操作系统来执行对应的文件操作。所以现在我们直接学习最底层的操作系统层面的文件操作。以后碰到什么类型的文件操作,本质上都是调用操作系统层面的某一个程序来执行,就能够触类旁通。
我们在上文中提到了C语言提供的文件操作函数,那么实际上fopen
,fclose
这两个C语言函数在内部是封装了两个系统调用:open
和close
的。
头文件:
#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:标识位,表示打开文件的模式
O_RDONLY:只读打开
O_WRONLY:只写打开
O_RDWR:读写打开
上述这三个模式必须指定一个且只能指定一个打开
O_CREAT:若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限(这个选项是一个建议选项)
O_APPEND:追加写
O_TRUNC:如果文件存在,那么打开的时候就清空
mode:创建新文件的权限
返回值:如果调用成功,返回打开文件的文件描述符,如果调用失败,则返回-1,同时设置错误码
头文件:
#incldue <unistd.h>
函数原型:
int close(int fd);
参数解释:
fd:file descriptor文件描述符
返回值:如果调用成功,返回0,如果调用失败就返回-1,同时设置错误码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define FILE_NAME "log.txt"
int main()
{
int fd = open(FILE_NAME, O_WRONLY | O_CREAT);//以只读的方式打开,如果文件不存在就创建文件(这里选项或起来表示同时传递)
if(fd < 0)
{
perror("open");
return 1;
}
printf("%s打开成功", FILE_NAME);
int ret = close(fd);//关闭文件
if(ret == -1)
{
perror("close");
return 2;
}
printf("%s关闭成功", FILE_NAME);
return 0;
}
但是,这里创建的log.txt
看起来怪怪的,他的权限是乱码,我们在使用fopen的时候就没有出现这种问题。这是因为在创建文件的过程中,需要确定创建的文件的权限(包括对文件拥有者,所属组和other的权限),在C语言提供的接口中的封装内置了传递创建权限。但是使用系统调用的时候需要我们自己手动传入,这也就是为什么要穿入mode
参数的原因。如果不传的话,那么创建的文件的权限就是随机值。
关于umask的问题
在上面的实例中,注意我们传入的权限是666,但是最终自动创建出来的文件的权限是664,这是因为普通用户的默认文件掩码是0002,在【Linux】基本知识和权限这篇文章中我们提到过
所以在新建文件的时候,文件的默认权限为
0666 & ~0002 = 0664
。如果想要让自己创建的文件权限被重新设置,可以在代码里加上umask(0)
把文件掩码设置为0。这样创建出来的文件的权限就是666了注意:这里我们更改的是子进程的文件掩码,它是由父进程shell继承来的,由于进程的独立性,所以我们更改子进程的文件掩码不会影响到父进程
2.3 文件读写
1. 文件写入:write
(1)普通写入
系统为文件的读写提供了两个系统调用接口:read
和write
,用于读写文件
头文件:
#include <unistd.h>
函数原型:
ssize_t write(int fd, const void *buf, size_t count);
参数解释:
fd:要写入的文件描述符
buf:写入内容的起始位置
count:需要写入的字节数
返回值:调用成功,返回写入成功的的字节数,如果返回值为0表示没写入任何东西,如果调用失败返回-1,同时设置错误码
如果要写入字符串的话,写入的长度不用加一,因为字符串的
\0
作为字符串结尾是C语言规定的,关操作系统什么事?因此我们在进行字符串的写入的时候,只需要写入字符串的有效内容即可。
2. 文件读取read
头文件:
#include <unistd.h>
函数原型:
ssize_t read(int fd, void *buf, size_t count);
参数解释:
fd:文件描述符
buf:从文件fd中读取count个字符到buf中
count:指定需要读取的字节数
返回值:如果调用成功,那么就返回读取的字节数(返回值为0表示已经读到文件末尾了),如果返回值小于传入的count,不是调用失败,而是由于一些原因导致没有读取到指定个数的字节。如果调用失败,就返回-1,同时设置错误码。
文件描述符的感性理解
在应用层面,由于使用了树状结构的目录,所以可以通过
文件路径+文件名
的方式来指定唯一文件,但是在操作系统层面,OS看不见这些内容。所以对于打开了的文件,需要给他们分配一个唯一标识符:文件描述符file descriptor
。
2.4 open中O_APPEND和O_TRUNC的使用
1. O_TRUNC
在上述的代码中,我们想要重新写入log.txt
中的内容
看到结果更我们想象的不太一样,多了一些东西。这是因为在写入的时候没有进行清空。需要在打开的时候加上O_TRUNC
选项
int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_TRUNC, 0666);
所以C语言提供的fopen接口中的“w”
选项在内部就是对open进行了这样的调用。
2. O_APPEND
如果想要对文件进行追加操作,在C语言中只需要在fopen中传入“a”/"a+"
即可。实际上在内部调用open的时候,只需要在传入的打开模式中或上O_APPEND
即可。
可以看到已经成功追加。
3. 文件操作的理解
1. 文件操作的本质就是进程对被打开文件的操作
2. 我们知道,一个进程可以打开多个文件,所以系统中肯定存在着大量的已经打开文件,那么这些已经打开文件是需要被管理起来的,也就是需要先描述,再组织。因此,操作系统为了把这些已经打开的文件管理起来,就实现了一个内核数据结构struct file
,在这个数据结构中包含了文件的大部分属性
3. 进程和被打开文件之间的关联:进程和被打开文件通过open的返回值来进行关联,所有的被打开文件都有一个唯一的返回值,我们把它叫做文件描述符(file descriptor)
4. 文件描述符
4.1 引入
既然我们说到,OS通过文件描述符管理所有被打开的文件,那么我们来看一看这个文件描述符到底是什么
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define FILE_NAME(number) "log"#number".txt"//拼接出文件名
int main()
{
int fd0 = open(FILE_NAME(0), O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fd1 = open(FILE_NAME(1), O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fd2 = open(FILE_NAME(2), O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fd3 = open(FILE_NAME(3), O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fd4 = open(FILE_NAME(4), O_WRONLY | O_CREAT | O_TRUNC, 0666);
printf("fd0=%d\n", fd0);
printf("fd1=%d\n", fd1);
printf("fd2=%d\n", fd2);
printf("fd3=%d\n", fd3);
printf("fd4=%d\n", fd4);
return 0;
}
我们可以看到文件描述符是一些小整数,这会让我们想到数组(数组的下标就是连续的小整数)那为什么是从3开始的呢?0,1,2去哪里了?
在学习C语言的过程中,我们了解过,有三个流是被默认打开的:stdin(标准输入),stdout(标准输出),stderr(标准错误),这三个流的类型也是
FILE*
类型。但是我们知道OS不会管FILE*类型,他只关注文件的fp,所以在FILE结构体内部肯定有一个字段是保存fd的
在/usr/include/libio.h
文件中我们能够找到C语言提供的FILE
结构体的声明,在里面能够找到_fileno
字段,这个字段就是该文件对应的fd。
所以我们可以通过stdout->_fileno
来查看对应的fd:
int main()
{
printf("stdin:%d\n", stdin->_fileno);
printf("stdout:%d\n", stdout->_fileno);
printf("stderr:%d\n", stderr->_fileno);
return 0;
}
所以,到此我们知道原来0,1,2默认被占用,我们的C语言封装了接口,同时也封装了操作系统内的文件描述符。
现在我们知道文件描述符就是0,1,2,3这样的小数字,那么文件描述符的本质到底是什么?
4.2 文件描述符的本质
一个文件,在被打开之前是存放在磁盘上的,如果需要对文件进行操作的话,就需要把文件加载到磁盘上,也就是打开文件的过程,文件打开之后就会被操作系统管理起来,我们之前说到了“先描述”是使用一个结构体struct file*
来描述一个被打开了的文件,“再组织”这件事情,操作系统是使用了一个结构struct files_struct
来组织,在这个结构中存在着一个数组叫做进程的文件描述符表strcut file* fd_array[]
用于存放struct file
的指针,至此我们就将已经打开的文件管理起来了。
那么还有一个问题:之前说了进程和被打开的文件需要关联起来,这是怎么关联的呢?
在进程的strcut task_struct
中有一个字段struct files_strcut *files
指向进程的文件描述符表,这样就把进程和被打开的文件管理起来了。
而我们所说的文件描述符,本质上就是strcut file *fd_array[]
的数组下标
经过了上述的结构构件,现在如果进程相访问一个被打开文件,就会经过以下过程
- 拿到要访问文件的fd
- 从
struct task_struct
中找到struct files_struct *files
- 再通过这个files找到对应的
fd_array
- 通过数组下标,也就是fd就能访问到对应文件的
struct file
结构体。
4.3文件描述符的分配规则
文件描述符本质上就是数组下标,那么这个数组的下表是怎么进行分配的呢?
我们知道,一个进程的的文件描述符表的前三个是默认被打开的,那么如果我们把0对应的关掉
那么如果我们把0对应的关掉,那么新打开的文件的fd就会被设置为0。
接下来试一试关掉stderr:
所以我们可以得出结论:文件描述符的分配遵循从小到大的顺序,在fd_array中找到最小的没有被分配的位置,对应的下标分配为当前打开文件的fd,所以在最开始我们的实例中打开的第一个文件的fd是3,是因为0,1,2被默认打开的stdin、stdout、stderr占用。
问题一:为什么在我们的代码中把stdin和stderr关掉了,没有对系统产生影响,后面还是能够正常输入?
我们知道,被打开的文件是需要和进程关联起来的,这种关联是通过进程PCB指向的fd_array中对stdin和stderr的地址的引用,我们使用close关掉对应的文件描述符,是在用户层面的,但是在操作系统层面使用了引用计数,用户层调用close只会让引用计数减一,只有当引用计数为0的时候OS才会关闭这个文件。
问题二:上述的演示中关掉了0和2,为什么没有关掉1?
接下来演示一下关掉1的现象:
根据前面的规则,我们知道当我们关闭了1之后,新文件的fd就会被分配给1。调用printf实际上就是把字符串写进stdout中,现在的stdout->_fileno对应的就是我们打开的文件,也就是log.txt,所以按道理来说这个输出就被写进了log.txt。
那么现在我们看一下log.txt中的内容有没有:
诶?为什么没有啊?不应该啊?
这其实是因为缓冲区没有被刷新的原因,在写入之后手动刷新一下缓冲区就行了
本来我们应该把打印往显示器文件里打印,最后经过我们的一系列操作把输出的结果写到了文件里。也就是本来应该写到显示器,却写到了文件,这种操作我们称之为重定向
5. 重定向
5.1 重定向的概念
我们接触过的重定向实际上也就三种:<输入
>输出
>>追加
重定向典型的特征就是:在上层用的fd不变情况下,在内核中更改fd对应的struct file*的地址
重定向的本质就是上层的fd不变,在内核中更改fd对应的struct_file*的地址
5.2 重定向的接口
在上述的例子中,我们看到了重定向的实现方式:关闭指定的流的文件描述符,然后把重定向目标文件打开,让它的fd被分配为指定的数值。但是这种手动实现的方式有点挫。
事实上,操作系统也提供了系统调用接口来做这件事:dup系列接口,这里我们只说dup2
。
头文件:
#incldue <unistd>
函数原型:
int dup2(int oldfd, int newfd);
参数解释:
oldfd:用于替换的数组下标
newfd:被替换的数组下标
返回值:调用成功,返回新的文件描述符,如果调用失败就返回-1,同时设置错误码
dup2的作用就是将
fd_array
数组中oldfd下标对应的元素拷贝到newfd下标对应的元素位置,最后留下的oldfd和newfd对应的内容都是原来oldfd中的内容。注意这个oldfd和newfd分别是什么。
5.3 重定向的实现
1. 输出重定向
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define FILE_NAME "log.txt"
int main()
{
umask(0);
int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_TRUNC, 0666);
if(fd < 0)
{
perror("open:");
return 1;
}
dup2(fd, 1);//使用dup进行输出重定向(也就是替换fd为1的位置)
printf("hello world!\n");
return 0;
}
2. 追加重定向
所谓的追加重定向,本质上就是将文件的覆盖写变成追加写,也就是把打开文件的方式由O_TRUNC
变成O_APPEND
。
那实现的代码也就显而易见了:
int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_APPEND, 0666);//打开文件的方式变成O_APPEND
3. 输入重定向
输出重定向的前提是文件必须存在
以读的方式打开文件,然后使用dup更换打开的文件的fd和stdin的fd。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define FILE_NAME "log.txt"
int main()
{
umask(0);
int fd = open(FILE_NAME, O_RDONLY);
if(fd < 0)
{
perror("open:");
return 1;
}
dup2(fd, 0);//使用dup进行输入重定向(也就是替换fd为0的位置)
char line[64];
while(1)
{
printf(">");//输入提示
char* ret = fgets(line, sizeof(line), stdin);
if(ret == NULL)
break;
printf("%s\n",line);//打印stdin读取到的内容
}
return 0;
}
5.4 之前实现的minishell添加重定向
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#include <errno.h>
#define NUM 1024//输入缓冲区大小
#define OPT_NUM 64//命令参数最大个数
#define NONE_REDIR 0//重定向类型的宏定义
#define INPUT_REDIR 1
#define APPEND_REDIR 2
#define OUTPUT_REDIR 3
#define ERROR_REDIR 4
#define trimSpace(start) {\
while(isspace(*start)) ++start;\
}while(0) //去空格的宏定义
char lineCommand[NUM];//输入缓冲区
char* argv[OPT_NUM];
int EXIT_CODE;
int redirType = NONE_REDIR;//重定向类型和目标文件的全局变量
char* redirFile = NULL;
void checkCommand(char* commands)//解析重定向命令,将重定向信息保存在redirType和redirFile中
{
assert(commands);
char* start = commands;
char* end = commands + strlen(commands);
while(start < end)
{
if(*start == '>')
{
*start = '\0';
++start;
if(*start == '>')
{
//append
redirType = APPEND_REDIR;
++start;
}
else
{
//output
redirType = OUTPUT_REDIR;
}
trimSpace(start);
redirFile = start;
break;
}
else if(*start == '<')
{
//intput
*start = '\0';
++start;
trimSpace(start);
//填写重定向信息(type和file)
redirType = INPUT_REDIR;
redirFile = start;
break;
}
else
{
++start;
}
}
}
int main()
{
while(1)//死循环,因为Shell要一只运行着
{
//初始化全局变量
redirType = NONE_REDIR;
redirFile = NULL;
//打印输出命令提示符
printf("[用户名@主机名 当前路径]$ ");
fflush(stdout);//由于打印命令提示符的时候没有换行,所以这里手动刷新缓冲区
//获取输入
char* str = fgets(lineCommand, sizeof(lineCommand) - 1, stdin);//最后一个位置用于在极端情况下保证字符串内有'\0'
assert(str);//判断合法性
(void)str;
lineCommand[strlen(lineCommand) - 1] = '\0';//消除行命令中的换行
checkCommand(lineCommand);//解析重定向命令
//命令解析(字符串切割)
argv[0] = strtok(lineCommand, " ");
int i = 1;
if(argv[0] != NULL && strcmp(argv[0], "ls") == 0)//识别ls,自动加上颜色选项
{
argv[i++] = (char*)"--color=auto";
}
while(argv[i++] = strtok(NULL, " "));//使用字符串切割函数依次拿到每个参数
if(argv[0] != NULL && strcmp(argv[0], "cd") == 0)
{
if(argv[1] != NULL)
{
chdir(argv[1]);
}
else
{
printf("no such file or directory\n");
}
continue;
}
if(argv[0] != NULL && strcmp(argv[0], "echo") == 0)
{
if(strcmp(argv[1], "$?") == 0)
{
printf("%d\n", EXIT_CODE);
EXIT_CODE = 0;
}
else
{
printf("%s\n", argv[1]);
}
continue;
}
//创建子进程
pid_t id = fork();
if(id == -1)
{
perror("fork");
exit(errno);
}
else if(id == 0)
{
//child
//用子进程来实现重定向的内容
//但是子进程如何执行重定向是由父进程来告诉子进程的(如何告诉?redirType和redirFile)
switch(redirType)
{
case NONE_REDIR:
break;//如果没有任何重定向的话就直接执行程序替换
case INPUT_REDIR:
{
int fd = open(redirFile, O_RDONLY);
if(fd < 0)
{//如果打开失败就直接返回
perror("open:");
exit(errno);
}
//使用dup2重定向
dup2(fd, 0);
}
break;
case OUTPUT_REDIR:
case APPEND_REDIR:
{
int flags = O_WRONLY | O_CREAT;
if(redirType == OUTPUT_REDIR) flags |= O_TRUNC;
else flags |= O_APPEND;
int fd = open(redirFile, flags, 0666);
if(fd < 0)
{
perror("open:");
exit(errno);
}
dup2(fd, 1);
}
break;
default:
printf("bug?\n");
}
//进程程序替换
execvp(argv[0], argv);
//执行到此处的时候,证明进程替换错误
perror("exec:");
exit(errno);
}
else
{
//parent
//进程等待
int status = 0;//退出状态
pid_t ret = waitpid(id, &status, 0);//阻塞等待
EXIT_CODE = (status >> 8) & 0xFF;
if(ret == -1)
{
perror("wait fail");
exit(errno);
}
}
}
return 0;
}
6. Linux下一切皆文件的理解
由于体系结构的限制,我们知道,一切的数据计算都需要经过内存再进行计算,计算之后再将结果写入/刷新到外设(键盘、显示器、磁盘、网卡等)。其中将数据读取到内存、将数据写到外设的过程就是就是I/O的过程。那么在操作系统层面,这些软硬件资源是需要被管理起来的。我们说管理的本质是“先描述,再组织”。所以操作系统为了将这些外设管理起来,就使用了一个struct file
结构体来抽象化。这个结构体内部包含了各种的文件属性,比如:外设类型,外设状态,外设的读写函数的指针等等。这些是OS层面的抽象的内容。
在硬件和驱动层面:对于每一个外设,他们都会实现一个驱动程序,在这个驱动程序中包含了这个硬件的读写方法,在OS层面上只需要将struct file
中的读写函数指针的字段指向这里的读写方法,就能够在OS层面调用到这些读写方法。同时,在OS层面不关心这些外设到底是什么,只关心这个文件的内容。这就是Linux下一切皆文件。
本节完
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!