1 文件
这里首先得理解一下文件,文件存放在磁盘中(磁盘是永久性存储介质,是一种外设,也是一种输入输出设备),磁盘上的文件的所有操作,都是对外设的输入和输出简称IO,linux下一切皆⽂件,无论是键盘、显示器、网卡、磁盘都可以抽象的理解为文件。
1.对于0KB的空⽂件是占用磁盘空间的(就是文件属性占用了空间)
2.文件是文件属性(元数据)和文件内容的集合(文件=属性(元数据)+内容)
3.所有的文件操作本质是文件内容操作和文件属性操作
理解:谁来操作文件?谁来管理文件 ?
文件操作的本质:进程对文件的操作。
管理者:磁盘的管理者是操作系统。
(另外在之前学习C语言时,我们通过C语言的函数接口,实现了对文件的操作,其实这些库函数只是方便用户使用,本质是封装的是系统调用接口来实现的)
2 文件路径
程序在当前路径下,系统怎么寻找到程序的当前路径?
ls /proc/[进程id] -l
可以查看当前正在运行进程的信息:
lrwxrwxrwx 1 iu iu 0 Aug 26 16:53 cwd -> /home/iu/io
lrwxrwxrwx 1 iu iu 0 Aug 26 16:53 exe -> /home/iu/io/test
cwd:指向当前进程运行目录的⼀个符号链接。
exe:指向启动当前进程的可执行文件(完整路径)的符号链接。
打开文件,本质是进程打开,所以,进程知道自己在哪里,所以文件不带路径,进程也知道。由此OS就能知道要创建的文件放在哪里。(之前讲的环境变量中,也存放着路径)
3 stdin&stdout&stderr
启动进程,C默认会打开三个输⼊输出流,分别是stdin,stdout,stderr
#include <stdio.h> extern FILE *stdin; extern FILE *stdout; extern FILE *stderr;
可以看到这是三个结构体指针。
所以向显示器上打印,除了使用printf,还有fwrtie,fprintf等,只需要修改一下写入对象为stdout即可。
#include <stdio.h>
#include <string.h>
int main()
{
const char *msg = "hello world\n";
fwrite(msg, strlen(msg), 1, stdout);
printf("hello world\n");
fprintf(stdout, "hello world\n");
return 0;
}
在之前学习C语言打开文件等操作时,还有就是权限问题:只读,只写,可读可写,追加等
r:只读
r+:可读可写
w:只写,如果文件不存在创建文件,每次写文件都会进行清空
w+ :可读可写
a:在文件末尾追加
a+:可读,在文件末尾追加
这里会和后面使用系统调用操作文件进行比较。
4 系统文件IO
4.1接口介绍 open
#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。
返回值:
成功返回新打开的文件符(fd)
失败返回-1
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
(这三个常量,有且只有一个)
O_CREAT : 若文件不存在,则创建它。(这里必须确定文件访问权限,也就是需要传入mode参数)
O_APPEND: 追加写
4.2 open返回值
这里还需要区分一下,库函数和系统调用的概念,也就是库函数其实就是对系统调用进行了一层封装,可以通过下图理解一下:
4.2.1 文件描述符fd
文件描述符其实就是一个整数。
在前面说进程启动会默认打开标准输入,标准输出,标准错误,其实对应的fd也就是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,1,2到底有什么含义,还是随机分配的数字?
从图片中可以看到,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有⼀个指针*files,指向⼀张表files_struct,该表最重要的部分就是包含⼀个指针数组,每个元素都是⼀个指向打开文件的指针。
得出结论本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。
可以查看内核源码观察确实如此:
4.2.2 文件描述符的分配规则
通过下面两段代码对比:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
//例子1
int main()
{
int fd = open("myfile", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
//例子2
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;
}
结果:分别是3和0;
首先第一段代码,进程启动默认打开0,1,2三个文件,所以再打开“myfile”时fd就为3了,第二段代码将一给关闭了,再打开“myfile”fd就变成了0;
得出结论:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
4.3 重定向
这里通过一段代码来说明:
#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);
return 0;
}
将1(标准输出流)关闭,打开“myfile”文件,再调用printf,发现并不会写入到显示器文件上了,而是写入到myfile中去了,这个就叫做输出重定向 (前面linux指令中 >, >> , <等符号也就是表示的是输出重定向,追加重定向,输入重定向)
4.3.1 重定向的本质
前面有一个结论:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。所以这里将1关闭之后,再打开一个文件,就会占据1下标(文件描述符)的位置,而printf默认是向1中写入,所以但printf并不知道文件描述符为1的文件已将改变了,所以就写入到新打开的文件中去了。
4.3.2 dup2系统调用
这就是系统提供的一个重定向的函数。
#include <unistd.h>
int dup2(int oldfd, int newfd);
将oldfd重定向到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);
while(true)
{
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;
}
这里通过将打开的文件重定向到1(标准输出)中,再从标准输入中读取,再利用printf向文件中写入。printf是C库当中的IO函数,⼀般往stdout中输出,但是stdout底层访问文件的时候,找的还是fd:1, 但此时,fd:1下标所表示内容,已经变成了myfifile的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成输出重定向。
5 linux中“一切皆文件”
这里我们可以从windows中理解,再windows中看到C盘,D盘中的文件,在linux下也是文件,但在linux下,进程,磁盘,显示器,键盘这样的设备也被抽象成文件,这样就可以通过访问文件的方式访问文件。
开发者仅需要使用⼀套API和开发工具,即可调取Linux系统中绝大部分的资源。举个简单的例子,Linux中几乎所有读(读文件,读系统状态,读PIPE(管道))的操作都可以用read 函数来进行;几乎所有更改(更改文件,更改系统参数,写PIPE(管道))的操作都可以用 write 函 数来进行。
这里通过观察file结构体来看看:
struct file {
...
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;
...
atomic_long_t f_count; // 表⽰打开⽂件的引⽤计数,如果有多个⽂件指针指向它,就会增加f_count的值。
unsigned int f_flags; // 表⽰打开⽂件的权限
fmode_t f_mode; // 设置对⽂件的访问模式,例如:只读,只写等。所有的标志在头⽂件<fcntl.h> 中定义
loff_t f_pos; // 表⽰当前读写⽂件的位置
...
} __attribute__((aligned(4)));
值得注意的是这里有一个f_op的结构体成员, f_op 指针指向了⼀个 file_operations 结构体,这个结构体中的成员除了struct module* owner其余都是函数指针。
file_operation 就是把系统调用和驱动程序关联起来的关键数据结构,这个结构的每⼀个成员都对应着⼀个系统调用。读取 file_operation 中相应的函数指针,接着把控制权转交给函数,从而完成了Linux设备驱动程序的工作。
每个设备都可以有自己的read、write,但一定是对应着不同的操作方法!!但通过 struct file 下 file_operation 中的各种函数回调,让我们开发者只用file便可调取Linux系统中绝大部分的资源。这也就是前面所说的“一切皆文件”。
6 缓冲区
缓冲区是内存空间的⼀部分。内存空间中预留了⼀定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。
6.1 缓冲区机制的作用
首先要知道,系统调用是需要消耗时间,直接通过系统调用对磁盘进行操作(读、写等),那么每次对文件进行⼀次读写操作时,都需要使用读写系统调用来处理此操作,即需要执行⼀次系统调 用,执行⼀次系统调用将涉及到CPU状态的切换,即从用户空间切换到内核空间,实现进程上下文的切换,这将损耗⼀定的CPU时间,频繁的磁盘访问对程序的执行效率造成很大的影响。
(这里举个例子,比如说:寄快递,如果是本人亲自送到目的地,那是不是浪费了自己很多时间,而如果我把它放到菜鸟驿站,让快递员去负责运输,那是不是本人就从这里面“解放出来了”,就可以去干更多的事)
为了减少使用系统调用的次数,提高效率,我们就可以采用缓冲机制。可以减少磁盘的读写次数,再加上计算机对缓冲区的操作大大快于对磁盘的操作,故应用缓冲区可大提高计算机的运行速度。
从磁盘里取信息,可以在磁盘文件进行操作时,可以⼀次从文件中读出大量的数据到缓冲区中,以后对这部分的访问就不需要再使用系统调用了,等缓冲区的数据取完后再去磁盘中读取。
6.2 缓冲类型
标准I/O提供了3种类型的缓冲区。
全缓冲区:这种缓冲方式要求填满整个缓冲区后才进行I/O系统调用操作。
(对于磁盘问件的操作通常使用全缓冲的方式访问)
行缓冲区:在行缓冲情况下,当在输入和输出中遇到换行符时,标准I/O库函数将会执行系统调用操作。
(当所操作的流涉及⼀个终端时(例如标准输入和标准输出),使行缓冲方式。因为标准 I/O库每行的缓冲区长度是固定的,所以只要填满了缓冲区,即使还没有遇到换行符,也会执行 I/O系统调用操作,默认行缓冲区的大小为1024)
无缓冲区:无缓冲区是指标准I/O库不对字符进行缓存,直接调用系统调用。
(标准出错流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显示出来)
以下为特殊刷新方式:
1. 缓冲区满时;
2. 执行flush语句;
通过下面一个例子:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
perror("open");
return 0;
}
printf("hello world: %d\n", fd);
close(fd);
return 0;
}
使用重定向,让本应该打印在显示器上的内容写到“log.txt”文件中,但我们发现, 程序运行结束后,文件中并没有被写入内容。
这里是因为1号重定向到磁盘文件后,隐式转换成全缓冲,所以这里\n,也不会刷新了,这里close了fd,缓冲区就没刷新到文件里面,可以使用fflush来强制刷新。
关闭文件之前,加一句下面代码就可以了。
fflush(stdout);
这里补充一个:
如果是重定向到2(stderr)是不带缓冲区的,就可以直接刷新到文件里面。
7 FILE
之前在调用C语言封装的文件操作的库函数,可以看到一些函数返回值是FILE*
如:
前面讲的系统调用,都是通过文件描述符fd来访问文件的,所以这个FILE结构体内,肯定封装这个fd成员。还有一点要注意,就是FILE中也存在语言级缓冲区。