Linux下基础IO

1 文件

这里首先得理解一下文件,文件存放在磁盘中(磁盘是永久性存储介质,是一种外设,也是一种输入输出设备),磁盘上的文件的所有操作,都是对**外设的输入和输出简称IO,**linux下一切皆⽂件,无论是键盘、显示器、网卡、磁盘都可以抽象的理解为文件。

1.对于0KB 的空⽂件是占用磁盘空间的(就是文件属性占用了空间)

2.文件是文件属性(元数据)和文件内容的集合(文件=属性(元数据)+内容

3.所有的文件操作本质是文件内容操作和文件属性操作

理解:谁来操作文件?谁来管理文件 ?

文件操作的本质:进程对文件的操作。

管理者:磁盘的管理者是操作系统。

(另外在之前学习C语言时,我们通过C语言的函数接口,实现了对文件的操作,其实这些库函数只是方便用户使用,本质是封装的是系统调用接口来实现的)

2 文件路径

程序在当前路径下,系统怎么寻找到程序的当前路径?

bash 复制代码
ls  /proc/[进程id] -l 

可以查看当前正在运行进程的信息:

bash 复制代码
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

cpp 复制代码
#include <stdio.h>
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;

可以看到这是三个结构体指针。

所以向显示器上打印,除了使用printf,还有fwrtie,fprintf等,只需要修改一下写入对象为stdout即可。

cpp 复制代码
#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

cpp 复制代码
#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,这里对应的一般物理设备是:键盘,显示器,显示器。

所以就可以通过下面代码进行从键盘读取向屏幕打印:

cpp 复制代码
#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 文件描述符的分配规则

通过下面两段代码对比:

cpp 复制代码
#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 重定向

这里通过一段代码来说明:

cpp 复制代码
#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系统调用

这就是系统提供的一个重定向的函数。

cpp 复制代码
#include <unistd.h>
int dup2(int oldfd, int newfd);

将oldfd重定向到newfd中。

例如:

cpp 复制代码
#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结构体来看看:

cpp 复制代码
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语句;

通过下面一个例子:

cpp 复制代码
#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来强制刷新。

关闭文件之前,加一句下面代码就可以了。

cpp 复制代码
fflush(stdout);

这里补充一个:

如果是重定向到2(stderr)是不带缓冲区的,就可以直接刷新到文件里面。

7 FILE

之前在调用C语言封装的文件操作的库函数,可以看到一些函数返回值是FILE*

如:

前面讲的系统调用,都是通过文件描述符fd来访问文件的,所以这个**FILE结构体内,肯定封装这个fd成员。**还有一点要注意,就是FILE中也存在语言级缓冲区。

相关推荐
爱瑞瑞15 分钟前
🐧深入浅出的认识 Linux 指令
linux·shell
星哥说事28 分钟前
开源综合性网络安全检测和运维工具-TscanClient
运维·web安全·开源
ajassi200029 分钟前
开源 java android app 开发(十一)调试、发布
android·java·linux·开源
小李飞刀李寻欢1 小时前
使用kubeadm部署Kubernetes(k8s)集群的步骤
linux·服务器·ubuntu·kubernetes·k8s
运维成长记1 小时前
阿里云实践创建实例步骤
linux·运维·服务器·阿里云·云计算
Kusunoki_D1 小时前
Python 实现 Web 静态服务器(HTTP 协议)
服务器·前端·python
THe CHallEnge of THe BrAve1 小时前
Linux检验库是否安装成功
linux·运维·服务器
Hello.Reader1 小时前
NGINX 四层 SSL/TLS 支持ngx_stream_ssl_module
运维·nginx·ssl
Estar.Lee2 小时前
如何在Debian中提高phpstorm的稳定性
运维·debian·api·免费api·phpstorm
藥瓿锻2 小时前
2024 CKS题库+详尽解析| 1. kube-bench 修复不安全项
运维·安全·docker·云原生·容器·kubernetes·cks