前言
在学习C语言时,我们接触过如fopen、fclose、fseek、fgets、fputs、fread、fwrite等函数,实际上,这些函数是对于底层系统调用的封装。C默认会打开三个输入输出流,分别是stdin,stdout,stderr。执行 man stdin
后,会展示如下描述:
#include <stdio.h>
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;
</stdio.h>
可以看到,这三个流类型都是FILE*,也就是说指向了某个文件。实际上,以上三者分别对应的文件为键盘、显示器、显示器。
那么,操作系统是如何管理文件,并进行文件IO呢?
- 文件描述符及基本IO接口介绍
1.1 什么是文件描述符
在第一讲中,我们知道了当进程被创建后,系统会给该进程分配对应的PCB,在Linux中,进程的PCB是task_stuct,里面有一项files_struct——打开文件表。打开文件表的源码如下:
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];/* 文件对象指针的初始化数组*/
};
进程是通过文件描述符(file descriptors,简称fd)而不是文件名来访问文件的,文件描述符是一个整数。
在打开文件表中,最重要的一项是fd_array[32],这是一个指针数组,通常, fd_array包括32个 文件对象指针,如果进程打开的文件数目多于32,内核就分配一个新的、更大的文件指针数组,并将其地址存放在fd域中,内核同时也更新max_fds域的值。
每当打开一个文件时,系统就会分配fd_array中的某项,将其指向打开的文件结构体。从下图我们可以看出,fd实际上就是fd_array的索引。只要有fd,就可以找到对应文件的位置。
1.2 基本IO接口
在认识返回值之前,需要先区分两个概念: 系统调用和库函数。 在用户程序中,凡是与资源有关的操作(如存储分配、进行I/O传输及管理文件等),都通过系统调用方式向操作系统提出服务请求,并由操作系统代为完成。在执行系统 调用的过程中,操作系统会由用户态进入到内核态。系统为了防止应用程序能随意修改系统数据,只给用户提供接口,用户要使用,那就通过提供的接口来调用。
fopen、fclose、fread、fwrite 都是C标准库当中的函数,我们称之为库函数(libc),而open close read write lseek 都属于系统提供的接口,称之为系统调用接口。实际上,库函数往往是对系统调用接口的进一步封装,方便程序员进行二次开发。
1.2.1 open/close接口
函数原型:
头文件:
#include <sys types.h></sys>
#include <sys stat.h></sys>
#include <fcntl.h></fcntl.h>
接口1: int open(const char *pathname, int flags);
:
接口2: int open(const char *pathname, int flags, mode_t mode);
接口3: int close(int fd);
如果打开的文件存在,则使用接口1,如果打开的文件不存在,则使用接口2。
pathname:待打开或创建的文件
flags:以何种方式打开。打开文件时,可以传入多个常量进行”或”运算。这些常量有:
0_RDONLY:只读打开;O_WRONLY:只写打开;O_RDWR:读写打开。这三个常量,必须指定且只能指定一个。
O_CREAT:若文件不存在,则创建该文件,需要使用mode选项,来指明新文件的访问权限。
O_APPEND:追加写入。
O_TRUNC:截断文件(清空文件内容)
O_NONBLOCK :使用非阻塞方式读写设备文件,如果不添加,默认情况下读写为阻塞方式。
以上的选项是按照按位或的方式进行组合的, O_RDWR|O_CREAT|O_APPEND
意思是以读写的方式打开文件,如果文件不存在则创建文件,打开文件后写入方式为追加写入。
mode:当创建一个新文件时,需要给文件设置权限,一般通过传递一个8进制的数字。关于文件权限,请读者自行查阅相关文章。
返回值:
创建成功返回一个文件描述符
创建失败返回-1。
1.2.2 read/wirte接口
ssize_t read(int fd, void *buf, size_t count);
fd:文件描述符
buf:将文件读到buf指向的空间中
count:期望读取的字节数
ssize_t write(int fd, const void *buf, size_t count)
fd:文件描述符
buf:将buf中的内容写到文件当中去
count:期望写入的字节数, size_t被定义为unsigned long。
返回值:返回读出或者写入的字节数。需要注意的是read和write的返回值都是有符号数, ssize_t被定义为long,出错的时候返回-1。有趣的是返回一个-1的可能性使得读到或者写入的最大值减小了一半。
通过下例来感受一下上面几个接口的使用:
#include <stdio.h>
#include <sys types.h>
#include <sys stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
char buff[1024];
int main()
{
int fd = open("wrfile",O_RDWR|O_CREAT|O_APPEND,0644);
if(fd == -1)
{
return 1;
}
else{
const char* str = "Hello world\n";
strcpy(buff,str);
write(fd,buff,strlen(buff));
printf("%d\n",fd);
close(fd);
}
return 0;
}
</string.h></unistd.h></fcntl.h></sys></sys></stdio.h>
执行该段代码后,可以看到如下输出结果
第一,打开文件后,将返回wrfile的fd,执行write函数,会将buff中的内容写入到wrfile中。
第二,输出wrfile的内容,发现如果只执行一次,输出一行”Hello world”,如果执行两次,输出两行”Hello world”,这是因为我们是以追加的方式打开的文件。
第三, 打开文件后,返回的fd号码是3。为什么会是3?这就与文件描述符的分配规则有关。
1.3 文件描述符的分配规则
根据我们在1.1中知道的,fd是fd_array[ ]的索引。通常,数组的第一个元素(索引为 0)是进程的标准输入文件,数组的第二个元素(索引为 1)是进程的标准输出文件,数组的第三个元素(索引为 2)是进程的标准错误文件,分别对应的键盘,显示器,显示器。在Linux中,万物皆文件,因此各种外设也会当做文件进行处理。
是不是瞬间明白了为什么用户自己打开第一个文件的时候分配的fd是3而不是0?是的,这是因为对于任何进程,标准输入文件、标准输出文件和标准错误文件会被默认打开。当再次分配的时候,操作系统会采用 最小未分配原则——即先分配当前fd_array中未被使用的最小索引。
如果我们先关闭了fd为1的文件,会是什么情况呢?请看下例:
#include <stdio.h>
#include <sys types.h>
#include <sys stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
close(1);
int fd = open("./myfile",O_RDWR|O_CREAT,0644);
if(-1 == fd)
{
return -1;
}else{
printf("The fd is : %d\n",fd);
fflush(stdout);
close(fd);
}
return 0;
}
</string.h></unistd.h></fcntl.h></sys></sys></stdio.h>
执行该段代码后,结果如下:
执行./test后,并没有在屏幕上打印出结果。但是当我们查看myfile文件中的内容时发现,原来是内容都输出在了myfile文件中。我们可以看到,打开myfile的fd为1,这是因为我们之前关闭了fd为1的文件,当打开新的文件时,系统会分配最小未使用的fd——1。
为什么执行printf后内容会输出到myfile中而不是屏幕上,这就需要提到输出重定向。
1.4 输入重定向与输出重定向的本质
1.4.1重定向的原理
printf是格式化输出函数,当调用printf时,数据会被默认输出到缓冲区中,当换行符或者缓冲区满时,会将数据刷新到stdout中。stdout是标准输出设备,其fd默认为1。
在上例中,关闭fd为1的文件后,当打开新的文件,会给其分配最小未使用的fd。此时执行printf函数,会将数据输出到磁盘文件myfile中,我们将这种现象称为输出重定向。常见的重定向有:>, >>,
Original: https://www.cnblogs.com/Grong/p/15522656.html
Author: 乌有先生ii
Title: Linux系统编程之文件IO
原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/585759/
转载文章受原作者版权保护。转载请注明原作者出处!