Linux基础IO-IO接口,文件描述符,重定向
Posted _light_house_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux基础IO-IO接口,文件描述符,重定向相关的知识,希望对你有一定的参考价值。
【Linux】基础IO
一、C语言中文件IO操作
1.C语言中的开关读写文件
在学习Linux中的IO操作之前,我们先来简单的回顾一下,C语言中我们常用的一些IO操作的接口。
1.1.fopen()
FILE* fopen(const char* path, const char* mode);
- 函数参数
path
:要打开的文件mode
:打开文件的方式r
:可读方式r+
:可读可写方式w
:可写方式,如果文件不存在,就创建一个文件。如果文件已经存在,就截断一个文件(清空文件内容)w+
:可读可写方式,如果文件不存在,就创建一个文件。如果文件已经存在,就截断一个文件(清空文件内容)a
:追加写,但是不可以读取内容。如果文件不存在,就创建一个文件。如果文件已经存在,就在文件的末尾开始追加写a+
:追加写,可以读取内容。如果文件不存在,就创建一个文件。如果文件已经存在,就在文件的末尾开始追加写
- 函数返回值
- 成功:返回一个文件流指针
FILE
- 失败:返回
NULL
- 成功:返回一个文件流指针
作用:以某种方式打开一个文件,并返回一个指向该文件的文件流指针。
1.2.fclose()
int fclose(FILE* fp);
作用:关闭传入的文件流指针指向的文件。
1.3.fwrite()
size_t fwrite(void* ptr, size_t size, size_t nmemb, FILE* stream);
- 函数参数
ptr
:写入文件的内容size
:往文件中写入的块的大小,单位为字节nmemb
:预期写入的块数stream
:预期写入文件的文件指针
- 函数返回值
- 成功:写入文件中的块数
- 常见用法
- 定义块的大小为1个字节,
nmemb
为向写入的字节数量,返回值为成功写入的字节数
- 定义块的大小为1个字节,
#include <cstdio>
#include <cstring>
#include <cstdlib>
int main()
{
FILE* fp = fopen("myfile.txt", "w");
if (!fp) {
perror("fopen");
exit(-1);
}
const char* msg = "hello Linux file\\n";
fwrite(msg, sizeof(char), strlen(msg), fp);
fclose(fp);
return 0;
}
运行结果:
注意:fopen()
中的path
不是执行程序的所处的路径,而是进程运行时做出的路径。
举例:
1.4.fread()
size_t fread(void* ptr, size_t size, size_t nmemb, FILE* stream);
- 函数参数
ptr
:将从文件读取的内容保存在ptr
所指向的空间中size
:定义读文件时块的大小,单位为字节nmemb
:期望从文件中读的块数stream
:预期读取文件的文件指针
- 函数返回值
- 成功从文件中读取的块的个数
- 常见用法
- 定义块的大小为1个字节,
nmemb
为向写入的字节数量,返回值为成功读取的字节数
- 定义块的大小为1个字节,
#include <cstdio>
#include <cstring>
#include <cstdlib>
int main()
{
FILE* fp = fopen("myfile.txt", "r");
if (!fp) {
perror("fopen");
exit(-1);
}
char buff[64];
fread(buff, sizeof(char), sizeof(buff) / sizeof(char), fp);
printf("%s", buff);
fclose(fp);
return 0;
}
运行结果:
2.stdin&&stdout&&stderr
默认情况下,C语言会自动打开两个输入输出流,分别是stdin
,stdout
,stderr
。
这三个流的类型都是FILE
,也就是文件指针类型。
既然是文件指针,所以这三个指针分别指向键盘,显示器,显示器。后面的系统IO会再详细的讲解这三个输入输出流。
3.三个标准流和IO接口
可以利用上面这三个标准流和C语言的IO接口,将字符串直接打印到显示器上。
#include <cstdio>
#include <cstring>
#include <cstdlib>
int main()
{
FILE* fp = fopen("myfile.txt", "w+");
if (!fp) {
perror("fopen");
exit(-1);
}
char buff[64];
fread(buff, sizeof(char), 12, stdin);// 从键盘中输入到buff中
fwrite(buff, sizeof(char), strlen(buff), stdout); // 从buff中写入到显示器上
fclose(fp);
return 0;
}
运行结果:
二、系统文件IO
其实除了C语言之外,很多语言都是自己的IO接口函数,但是下面我们要谈论的就是系统给我们提供的IO接口,也就是系统级别的IO接口。
1.系统级别的开关读写文件
1.1.open()
// 在打开的文件已经存在的时候
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_TRUNC
:截断文件(清空文件内容)O_CREAT
:文件不存在则创建文件O_APPEND
:追加方式O_EXXL | O_CREAT
:如果文件存在,则打开文件失败
- 原理
- 可以使用按位或的方式进行组合:如打开并创建只写文件
O_WRONLY | O_CREAT
- 本质是利用了位图的方式来表示每一种的方式
- 可以使用按位或的方式进行组合:如打开并创建只写文件
- 必选项
mode
:当打开一个新开的文件的时候,需要给一个文件设置权限,需要设置一个8进制的数字。这个和umask
也会有关系
- 函数返回值
- 成功:返回一个文件描述符(后面介绍)
- 失败:返回-1
作用:打开一个文件
1.2.close()
int close(int fd);
- 函数参数
fd
:文件描述符
作用:关闭一个文件
1.3.write()
ssize_t write(int fd, const void* buf, size_t count);
- 函数参数
fd
:文件描述符buf
:将buf中的内容写到文件中count
:期望写入的字节数
- 返回值
- 返回的字节数
代码示例:
#include <cstdio>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <cstring>
#include <cstdlib>
int main()
{
// 创建一个权限为666的权限
umask(0);
int fd = open("file.txt", O_WRONLY | O_CREAT, 0666);
if (fd < 0) {
perror("open");
exit(1);
}
// 将msg写入file.txt中
const char* msg = "I am studing Linux IO\\n";
write(fd, msg, strlen(msg));
close(fd);
return 0;
}
运行结果:
1.4.read()
ssize_t read(int fc, void* buf, size_t count);
- 函数参数
fd
:文件描述符buf
:将文件中的内容读到buf中count
:期望写入的字节数
- 返回值
- 返回的字节数
代码示例:
#include <cstdio>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <cstring>
#include <cstdlib>
int main()
{
int fd = open("file.txt", O_RDONLY);
if (fd < 0) {
perror("open");
exit(1);
}
char buff[64];
read(fd, buff, sizeof(buff));
printf("%s", buff);
close(fd);
return 0;
}
运行结果:
2.系统文件IO VS C文件IO
上面的fopen
、fclose
、fread
、fwrite
都是C标准库中的函数,我们统称为库函数。
而open
、close
、read
、write
都是系统提供的接口,我们称之为系统调用接口。
如下图:
右下图可知,系统调用接口在lib
库函数之下,所以库函数中其实求出对系统调用接口的二次封装。
因为库函数是系统函数的一层封装,因此库函数对文件操作的时候,必然会使用系统调用接口。每打开一个文件所获得的文件指针FILE
都有一个文件描述符fd
与之对应。
为什么不适用系统调用接口,而是只使用库函数的IO调用接口?
1.虽然库函数有函数调用的开销,但是系统调用比库函数调用还要慢,因为它需要把上下文环境切换成为内核模式。
2.系统调用与操作系统是相关的,所以系统调用接口没有跨平台的可移植性。
3.一般读写文件都是要操作大量的数据,而库函数调用要大大减少系统调用的次数。这是因为缓冲区的技术,内核缓冲区是全缓冲,只有当缓冲区写完之后或者结束之后,才会将缓冲区中的内容写入文件中。
3.文件描述符fd
在上面open
的接口中,我们提到了fd
,这个也是open
接口的返回值。而write
和read
接口也是通过fd
这个参数使得文件可以读写,可以说fd
是整个系统IO的灵魂,所以接下来,我们需要好好地理解一下fd
。
3.1.什么是文件描述符
在Linux下一切皆文件,而大量的文件需要被高效的组织和管理,因此就诞生了文件描述符fd
(file descriptor)。
文件描述符是内核为高效的管理已经被打开的文件所创建的索引,它是一个非负整数,用于指代被打开的文件,所有执行I/O操作的系统调用都是通过文件描述符完成的。
进程和文件之间的对应关系是如何建立的?
由图可知:文件描述符就是从0开始的正整数。但我们打开一个文件的时候,操作系统都需要创建一个数据结构来描述这个文件。所以struct file
结构体就应运而生了,它就是表示打开的一个文件对象。
当进程执行open
函数的时候,必须要让进程和文件关联起来。所以在每一个进程的PCB
中都是一个struct files_struct* files
指针,它指向一张表files_struct
,这个表中有一个指针数组fd_array[]
,其中指针数组的每一个元素都是一个指向struct file
结构的struct file*
指针,而这个文件指针就指向打开的文件。
注意:向文件写入数据后,数据其实先写入对应文件的缓冲区当中,只有当将缓冲区中的内容刷新到磁盘当中时才算真正地写入到文件当中。
小总结:
- 所以本质上文件描述符就是
struct file_struct
结构中fd_array
数组的下标。而只要拿到了这个文件描述符,就可以找到对应的文件。
什么是进程创建会默认打开文件的0,1,2?
在Linux中,进程是通过进程描述符fd来访问文件的,文件描述符实际上是一个整数。在程序刚启动的时候,默认有三个文件描述符,分别是:0(代表标准输入stdin
),1(代表标准输出stdout
),2(代表标准错误stderr
)。对应的物理设备就是:键盘,显示器,显示器。
这三个文件设备都有自己对应的struct file
系统会默认的生成这三个结构体,并使用双链表将他们连接起来,并且将struct file
的地址放入到struct file* fd_array[]
数组的对应在0, 1, 2位置上。这个默认生成结构体并将地址放在fd_array
数组的过程就叫做默认打开了标准输入流,标准输出流和标准错误流。
补充:磁盘文件和内存文件的区别?
上面说的都是在操作进程打开的文件,正是因为操作系统中有大量的进程打开了大量的文件,所以需要使用struct file
和struct files_struct
这样的结构体去管理这些文件。而这些文件都是在内存中加载的文件,所以我们称之为**「内存文件」**。
如果一个文件储存在磁盘当中,我们就称之为「磁盘文件」。这两种文件的关系就是当一个磁盘文件被加载到内存当中的话,就成为了内存文件。
磁盘文件由两部分构成:「文件内容」和「文件属性」。文件内容就是文件中的数据内容,而文件属性(元信息)就是一个文件的基本信息。这就像是去超市买一盒牛奶,其中的牛奶就是文件内容,而牛奶的包装盒上的牛奶成分分析表就是文件属性。(在后面的文件系统中,还会详细地介绍磁盘文件)
文件加载到内存时,一般先加载文件的属性信息,然后将文件内容放入缓冲区中,延后式的慢慢加载内存。
3.2.如何创建文件描述符
进程通过文件描述符最常见的方式就是通过系统调用接口open
或者是从父进程继承过来的。
虽然文件描述符对于每一个进程的PID
都是唯一的,但是每一个进程都是一个进程描述表struct files_struct
,用于管理进程描述符,当使用fork
创建子进程的时候,子进程会获得父进程进程描述表的一个副本,所以子进程可以拿到父进程的进程描述符,因此就可以打开父进程所有的文件。
3.3. 文件描述符的分配规则
我们先上结论, 文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
如果再打开一个新的文件的话,就分配一个最小的没有使用的文件描述符fd。因为默认打开了0, 1, 2,所以新的文件描述符就应该从3开始的。
**思考:因为文件描述符也是可以使用close
关闭掉的,所以如果我们先将stdin
对应的0关闭掉的话,然后在此时打开一个新的文件的话,则这个文件对应struct file
的文件描述符就应该是0,此时这个文件就变成了标准输入。**如果我们向标准输入中输入一些内容的话,其实就输入到了这个文件当中。这个原理就和重定向的原理很像,只不过重定向要比这个原来还要复杂一点,但是这个可以帮助我们学习重定向。
#include <cstdio>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <cstring>
#include <cstdlib>
int main()
{
close(0); // 关闭stdin
int fd = open("file.txt", O_RDONLY);
if (fd < 0) {
perror("open");
exit(1);
}
printf("%d\\n", fd);
close(fd);
return 0;
}
运行结果:
3.3. 文件描述符与打开文件之间的关系
每一个文件描述符都对应着一个打开的文件,同时,不同的文件描述符也可以指向同一个文件。
同一个文件可以被同一个进程打开多次,也可以被不同的进程打开。
系统为每一个级进程都创建了一个文件描述表。内核中维护了三种文件描述表。
1.进程级别的文件描述表
进程级别的文件描述符表的每一条目都记录了单个文件描述符的相关信息。
struct files_struct {
atomic_t count; /* 共享该表的进程数 */
rwlock_t file_lock; /* 保护以下的所有域,以免在tsk->alloc_lock中的嵌套*/
int max_fds; /*当前文件对象的最大数*/
int max_fdset; /*当前文件描述符的最大数*/
int next_fd; /*已分配的文件描述符加1*/
struct file ** fd; /* 指向文件对象指针数组的指针 */
fd_set *close_on_exec; /*指向执行exec( )时需要关闭的文件描述符*/
fd_set *open_fds; /*指向打开文件描述符的指针*/
fd_set close_on_exec_init;/* 执行exec( )时需要关闭的文件描述符的初 值集合*/
fd_set open_fds_init; /*文件描述符的初值集合*/
struct file * fd_array[32];/* 文件对象指针的初始化数组*/
};
2.系统级别的文件描述表
内核对所有打开的文件有一个系统级别的文件描述符表。有时也称为打开文件描述符表,并将表格中各条目称为打开文件句柄。一个打开文件句柄存储了这个打开文件的全部相关信息。
struct file
{
struct list_head f_list; /*所有打开的文件形成一个链表*/
struct dentry *f_dentry; /*指向相关目录项的指针*/
struct vfsmount *f_vfsmnt; /*指向VFS安装点的指针*/
struct file_operations *f_op; /*指向文件操作表的指针*/
mode_t f_mode; /*文件的打开模式*/
loff_t f_pos; /*文件的当前位置*/
unsigned short f_flags; /*打开文件时所指定的标志*/
unsigned short f_count; /*使用该结构的进程数*/
unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin;
/*预读标志、要预读的最多页面数、上次预读后的文件指针、预读的字节数以及
预读的页面数*/
int f_owner; /* 通过信号进行异步I/O数据的传送*/
unsigned int f_uid, f_gid; /*用户的UID和GID*/
int f_error; /*网络写操作的错误码*/
unsigned long f_version; /*版本号*/
void *private_data; /* tty驱动程序所需 */
};
3.文件系统的inode
表
作用:保护了文件系统的相关信息。
不同级别的文件表述表的关系:
3.4.文件描述符fd与文件指针FILE的区别
在linux系统中打开文件就会获得文件描述符,它是一个数组的下标。每个进程控制块(PCB)中保存着一份文件描述符表,文件描述符就是文件描述符表的索引,每个表项都有一个指向打开文件的文件指针,这个文件指针指向进程用户区中的一个被称为FILE
的数据结构。FILE
结构包含一个缓冲区和一个文件描述符。而文件描述符是文件描述符表的一个索引。
4.重定向
理解了文件描述符后,就可以讲一讲重定向的原理了。
4.1.重定向原理
**简单来说:重定向的原理就是修改了文件描述符和打开文件的对应关系。**接下来的三个例子会进一步的帮助你理解这句话。
4.1.1.输入重定向原理
原本文件描述符指向标准输入流文件,而现在我们先将标准输入流文件关闭,然后再打开一个文件,这时文件描述符0就分给了新打开的文件了。
举例:
scanf默认是从标准输入中获取内容,如果打开的文件的文件符为0的话,那么就从打开的文件中获取内容。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cstdlib>
int main()
{
close(0);
int fd = open("file.txt", O_RDONLY | O_CREAT, 0644);
if (fd < 0) {
perror("open");
exit(1);
}
char str[1024];
while (scanf("%s", str) != EOF)
printf("%s", str);
close(fd);
return 0;
}
运行结果:
4.1.2.输出重定向
原本文件描述符指向标准输出流文件,而现在我们先将标准输出流文件关闭,然后再打开一个文件,这时文件描述符1就分给了新打开的文件了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bIkuVqc4-1635688996871)(D:\\github\\gitee\\linux-study\\【Linux】基础IO.assets\\1635427605224.png)]
举例:
printf
标准输出默认是往显示器上打印内容,如果打开的文件的文件符为1的话,那么就往打开的文件中打印内容。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cstdlib>
int main()
{
close(1);
int fd = open("file.txt", O_WRONLY | O_CREAT, 0644);
if (fd < 0) {
perror("open");
exit(1);
}
Linux系统编程:基础IO 上简单复习C语言文件接口 | 学习系统文件接口 | 认识文件描述符 | Linux下,一切皆文件 | 重定向原理
Linux系统编程:基础IO 壹简单复习C语言文件接口 | 学习系统文件接口 | 认识文件描述符 | Linux下,一切皆文件 | 重定向原理
Linux系统编程:基础IO 上简单复习C语言文件接口 | 学习系统文件接口 | 认识文件描述符 | Linux下,一切皆文件 | 重定向原理