Linux总结(二)

文章详细阐述了文件的概念,包括其内容和属性,以及对文件的操作如读写。提到了C/C++程序中的标准输入、输出和错误流,并解释了进程如何通过接口访问硬件上的文件。讨论了文件系统调用接口,如fopen、open、close、read和write,强调了跨平台性和系统调用与C库函数的区别。此外,还涉及了文件描述符、缓冲区的作用、重定向以及一切皆文件的Linux哲学。
摘要由CSDN通过智能技术生成

基础IO

1.什么叫文件?

我们需要在操作系统的角度理解文件。

文件 = 文件内容 + 属性(所以即使是空文件,也会占空间,因为我们是需要保存文件属性的,属性也是数据,所以占空间) 

2.我们对文件的操作无非就是对内容操作、对属性操作。

3.文件在磁盘(硬件)上放着,我们访问文件,需要:写代码->编译->形成exe->运行->访问文件(本质是进程在访问文件)

进程访问文件是需要通过接口来访问的,接口有语言给我们的接口和系统接口,语言上的接口都封装着系统接口

往硬件里写入,只有谁有权利呢?

操作系统

C/C++程序默认会打开三个文件流,叫做标准输入(stdin),标准输出(stdout),标准错误(stderr)

我们以前讲过Linux下一切皆文件,那么键盘和显示器其实也可以被看做文件,以后再解释

 往显示器上打印,是不是在写显示器文件的内容,从键盘里读,是不是从键盘文件里读内容。是。因为我们的C/C++程序在编译的时候,它把打开的代码(文件)都内置到代码里了,这就叫做默认情况下会打开这些东西(标准输入、标准输出、标准错误),而标准输入对应的默认设备就是键盘,标准输出和标准错误默认对应的设备都是显示器,其都分别对应着C语言的(键盘对应)stdin,(显示器对应)stdout和stderr

2.文件的所有操作,也就是a.对内容操作 b.对属性操作

文件是存在磁盘的,磁盘是硬件,只有操作系统才能真正访问磁盘(硬件)。我们要读写C/C++程序,需要先把程序变成进程,

3.文件在磁盘(硬件)上放着,我们访问文件需要:先写代码,然后编译,形成exe程序,exe程序运行起来才能访问文件。所以说,访问文件的本质是进程在访问文件。进程访问文件是需要通过接口来完成。我们之前学的基本上都是语言上的接口,

要向硬件里写入,只有操作系统(硬件的管理者。其实是操作系统通过驱动程序什么的,但归根结底,只能是操作系统)才有权利。

如果我们这些普通用户也想向硬件中写入呢?此时就必须让操作系统提供接口才可以。操作系统需要给我们提供文件类的系统调用接口。

系统提供的系统调用接口使用起来是有难度的。而语言上会对系统提供的文件类的系统调用接口做一些封装,其是为了让接口可以更好的使用,因此导致了不同的语言有不同的语言级别的文件访问接口(每个语言基本都不一样),但是,都因为封装的是系统接口,所以原理是一样的,只是封装出来看起来是不一样的而已。

我们学习系统的文件调用的接口,因为这样的接口只有一套。

我们学习操作系统层面的文件接口,是因为在Linux上,这样的接口只有一套,为什么只有一套呢,因为我们所使用的操作系统只有一个(不是说只有一个操作系统,而是将来我们写的代码在Windows系统下就只是在Windows这一个操作系统,在Linux系统下跑就只有Linux这一个系统)

文件类的系统调用接口不具有跨平台性。如果语言不提供对文件系统接口的封装,所有访问文件的操作都必须直接使用操作系统的接口(Windows和Linux的文件接口是不同的)。所以,一旦使用系统接口编写所谓的文件代码,就无法在其他平台中直接运行了,不具备跨平台性。所以语言级别上的接口需要做一些程度的封装,来保证跨平台性。

那么C/C++在封装时是如何保证其跨平台性的呢?

把所有的平台的代码,都实现一遍,用多态,写基类等等,分别提供不同系统的接口,再采用条件编译,动态裁剪等

4.显示器是硬件,那么我们printf向显示器打印(也是一种写入),我们立马能看到结果,而写入到文件,没有直接往显示器直观,但是实际上向显示器写入和向磁盘上写入,没有本质区别

5.Linux下一切皆文件

显示器:printf/cout本质是就是一种write

键盘:scanf/cin本质是一种read

程序想写想读,是需要把程序加载到内存的。

例如:键盘是把我们所对应的数据输入交给内存(即内存从键盘这个文件中读取数据),然后内存把数据从我们对应的系统当中写入到我们的文件或显示器当中。

 那么什么叫文件呢?

站在系统的角度,能够被input读取,或者能够被output写出的(设备)就叫做文件。

我们狭义上说的文件:普通的磁盘文件

广义上说的文件包括:显示器、键盘、网卡、声卡、显卡、磁盘,几乎所有的外设都可以称之为文件

这些文件都具备能够被读或被写的特点,所以我们把它们统一都叫做文件

1.复习一下C接口

fopen的使用

FILE *fopen(const char* path, const char *mode);

FILE *fopen(int fd,  const char *mode);

FILE *fopen(const char* path, const char *mode, FILE *stream);

const char* path:需要打开的对应的文件

const char *mode:打开这个文件的模式

FILE*:文件指针、文件句柄

mode:

r:只读

r+:读写

w:写入(从文件最开始处写,如果存在,先被清空再写)

w+:读写打开(如果文件不存在,那么这个文件会先被创建)

a:向文件的结尾处写(追加)

a+:

(truncate是清空的意思)

perrno,strerror

当使用fopen与w组合时,若是没有的文件,那么其会创建文件到当前路径。

注意,当前路径不代表一定是源代码所在路径。

 

 这个log.txt是谁创建的呢?

(操作系统创建的,)不是我们的程序创建的,而是其形成的进程,进程通过系统接口才创建的,只有我们把程序运行起来,文件才能被创建出来。

ls /proc/13565查看当前正在运行的这个进程的进程信息

 exe:这个进程对应的磁盘上的可执行程序对应的路径。

 cwd:这个进程的当前工作目录

 当一个进程运行起来的时候,每个进程都会记录自己当前所处的工作路径。这个路径就叫做当前路径

我们fopen时,并没有写路径,它是如何实现的呢?

实际在打开文件时,是一个进程在打开,所以这个文件路径的形成实际上是使用进程的cwd,然后在底层做一个拼接,把文件名拼接到cwd后,这样就有了该文件创建后,该文件的cwd(也就是进程运行在哪个路径下,形成的文件就在哪个路径下)

当前路径:当一个进程运行起来时,这个进程所处的工作路径

//这里的1是'\0',要不要加呢? 

往文件中写入时,要不要加'\0',不要,因为'\0'结尾只是C语言的规定,文件不需要遵守(如果+1,我们会发现文件中多了一个乱码) 

文件已经被我们写到文件里了,文件已经在磁盘上了,和C语言已经没有关系了。所以加了'\0',会出现乱码。

我们要记住,'\0'是C语言的规定,文件保存的只是有效数据,什么叫有效数据呢?'\0'不算有效数据,它只是C语言中标定字符串结束的标识符,这个标识符不算文件的内容,所以在文件中它不算有效数据,也就不需要它,(在C语言中使用文件操作函数时是不需要加'\0'的)

我们验证发现,里面的东西没了。(当我们以w方式的打开文件的时候,先做的不是写入,而是先将文件清空)例如:即只是单纯的打开和关闭文件一次,该文件中的内容都会被清空掉,也就说明,当我们以写方式打开文件的时候,先做的是清空。

其类似于:

会发现log.txt里又什么都没了

这个>其实是一个命令,之所以这样,是因为它是先把这个文件打开,打开的话会先清空这个文件(它其实也是用fopen写的,因为Linux是用C语言写的)。

用a方式打开,a是追加,不断在原始文件的最后追加

这就是a和w的区别

fgets是C语言提供的接口,其会自动在字符结尾添加'\0',所以sizeof不用-1

按行读取

 

第二十一节 1:19:10 -  1:22:00想自己实现一个myfile 文件名   的mycat命令

小总结:

fopen以w方式打开文件,默认先清空文件(注意,是在fwrite前,即一打开就清空了)

fopen以a方式打开文件,表示追加,即不断地向文件中新增内容

C语言会默认打开三个标准的输入输出流:

sdtin(现在其一般对应键盘)

stdout(现在其一般对应显示器)

stderr(现在其一般对应显示器)

这三个东西是什么呢?它们其实都是文件指针。

键盘、显示器其实也是文件,也是FILE*的。

fprintf、fput、fputs、fwrite、

2.直接使用系统接口

C库函数

系统调用

它们两个之间,C库函数在上,系统调用在下,一个是语言提供的接口,一个是系统提供的接口,C库里的所有接口,底层一定会调用系统接口,因为它是必须要经过操作系统的。

下面我们要学的几个系统接口:open、close、read、write

 注意,系统调用的flags(选项)的传递是有讲究的

flags:

O_APPEND:追加

O_CREAT:如果文件不存在,则会被创建

O_TRUNC:清空。如果要打开的这个文件已经存在了,并且是一个常规文件,打开它的目的是为了写入,那么这个文件将会被清空为0。

用open打开文件时,O_RDONLY、O_WRONLY、O_RDWR,三个至少带一个。

上面的这些纯大写的都是宏定义,

 函数原型中flags只有一个,我们是如何给函数传递标志位的呢?我们如何传多个呢?

一个int类型有32个比特位,每个比特位分别可以为1或0,也就是说我们只用表示是和否就可以,也就是让每一个标记位对应占一个比特位。系统常用的标志位写法就是每一个标记位对应占一个比特位

如果open打开成功,会返回file destructor,其是一个整数

file descriptor:文件描述符

description,失败了返回-1,并且error会被设置

 

它告诉我们没有这个文件。 

因为open没有创建这个文件。所以如果需要顺带创建文件(文件不存在)的话,需要加上O_CREAT。

那么怎样添加呢?

按位或就可以

倒是创建出来了,但是它的权限为什么是这样的?

我们可以用这个接口

int open(cosnt char *pathname, int flags, mode_t mode);//多的这个参数叫做权限

为什么少了一个w呢?

因为系统里有默认的umask,把那个给过滤了。我们可以设置整个系统的umask,也可以为单个进程设置umask

 如果文件已经存在了,用那个有两个参数的open就可以。

 

 write

向fd标识的这个文件里写buf,写count个字符

要不要+1,不用。

write可不是清空再写入。而是直接覆盖式写入(直接从头写入),我们需要加O_TRUNC。

 这样才能达到效果

当想使用a时,需要添加O_APPEND。记得删掉O_TRUNC

 这样就可以追加了

 

read

 在fd标志的文件里读count个字符到buf里。返回值是ssize_t。ssize_t的意义是我们实际读到的字节数。其是一个有符号的整型。

read是二进制读取,是按照流的方式来读的,它读上来的内容是字符。

 

3.分析系统接口的细节,引入fd(文件描述符)

close

 

 0,1,2是文件标识符

0,1,2之间是有一些关联关系的

我们发现,向stdout里打、向1里面打,都能把数据写到显示器中 

我们可以看到也是输入什么读什么。 

FILE是一个结构体,是C标准库提供的。

不是所有的C语言接口都调系统调用。但是C中有关文件的库函数,其内部一定会调用系统调用。在系统角度,认识FILE还是fd?

系统只认识fd。所以FILE结构体里一定封装了fd。

如何证明stdin,stdout,stderr内部有fd呢?

 

4.周边文件(fd的理解,fd和FILE的关系,fd分配规则,fd和重定向,缓冲区)

fd是什么呢?

进程要访问文件,必须先打开文件,一个进程可以打开多个文件,一般而言,进程:文件=1:n。文件要被访问,前提是加载到内存中,才能被直接访问。

不仅仅包含属性,还有权限、链接属性等等

如何找文件?系统通过数组下标,在数组中做哈希索引,索引之后找到了被打开的文件对象。

所谓的fd本质是一个数组下标。上图是在操作系统内部。当使用open接口时,1.创建一个文件对象2.在数组里找一个没被使用的地方把地址放进去3.把对应的数组下标返回给用户4.用户用fd就可以找到文件,调用对应的接口,根据当前进程的PCB找到数组,然后根据数组索引到对应的文件对象,而只要找到了文件对象,文件对象里包含了文件的所有内容(文件属性、文件缓冲区、文件读写方法等)

当我们打开文件时,文件的很多属性就已经被填充到PCB结构体当中了。

struct file包含了文件的所有属性。

进程和文件的对应关系:

进程和文件是通过指针数组来维护的。文件描述符的本质是数组下标。

通过FILE*,找到文件描述符,将我们对应的数据向文件描述符中写入

文件有被进程打开的文件(也叫内存文件,因为文件凡是被打开,就已经被加载到内存里了,至于加载的是内容还是属性,看具体情况)、没有被打开的文件(又叫磁盘文件,也就是在磁盘上等着而已)。

当打开文件时,文件的属性从哪来呢?

从其构建的结构体来

为什么一定是在内存中打开的文件呢?

因为打开文件就是为了读写,读写是要通过代码来访问,通过代码访问的前提是代码必须被执行,执行的过程中去访问这个文件,根据冯诺依曼体系,执行代码的一定是CPU,访问你的数据必须得先加载(load)到内存中,如果没有load到内存中,软件就跑不起来,所以一个软件要被访问,必须要在内存中打开。

当一个进程被创建,其PCB内有一个指针                 

第二十二节00:30:00-:1:00:00笔记没做好

有一些问题:

1.文件方法。如何向文件中写?

2.文件内部必须有对应的内存区域供我们读写。

文件描述符fd

默认为0、1、2

Linux进程默认情况下会有三个缺省打开的文件描述符,分别是标准输入(stdin 0)、标准输出(stdout 1)、标准错误(2)

0、1、2一般对应的物理设备是:键盘、显示器、显示器

文件描述符是先分配3再分配4吗?

0、1、2是被使用的

当前路径:进程所对应的路径 

 printf默认往stdout里打印

上面的原理是什么呢?(其类似于输出重定向,那么输出重定向的原理是什么呢?)

当我们打开进程时,创建了PCB,其内有一个指针指向files_struct(该结构体里面包含了一个数组,fd_array,也就是文件描述符表,它是一个指针数组)。我们把键盘、显示器等打开也需要在内核中重复上述操作。每个进程的打开都会默认打开着标准输入、标准输出、标准错误,这三个文件此时分别对应键盘、显示器、显示器,这三种文件在内核中都是一个struct file对象(变量),这时,files_struct里的那个数组就把这三个文件的地址收过来并且返回了其下标。

如果在打开文件前关闭了fd为1的文件,其里面的地址会被清成null,然后我们open时,操作系统会发现1中为null,就会将log.txt的地址填充进去。所以我们写的内容不会是到标准输出里了,而是写到了文件中。这就是重定向的原理。

重定向的本质其实是在操作系统内部更改fd对应的内容的指向。

 输入重定向:

 我们发现fgets居然不是等待我们输入,而是直接读取了log.txt里的一行

 

dup2

其本质是做指针的拷贝,将旧fd该数组指向的地址代表的内容(不是拷贝指针过去)拷贝给新fd。那么新fd的指针指向就可以被关闭了。

 2:05:00-2:18:00为什么dup2显示的一会显示,一会不显示呢?以后讲 

Linux是用C语言写的,那么如何用C语言实现面向对象,甚至是实现运时多态?

C语言的struct里不能包含成员函数,如果我们想要在C语言struct里增加成员函数,我们可以把函数指针放到结构体里,这个函数指针在执行前就已经指向了各自对应的函数。

如何理解一切皆文件

一切皆文件是Linux的设计哲学,体现在操作系统的软件设计层面。

底层不同的硬件,一定对应的是不同的操作方法。read对应I,write对应O

所有的设备都有自己的read和write,但是,代码的实现一定是不一样的。虽然代码实现方式不同,但是在Linux实现时,其本质操作基本都是一样的,其看待所有文件的方式,都是struct file,然后传指针给系统接口,通过不同方式实现不同要求。

上层是操作系统实现、维护的,下层是驱动开发

 Linux下一切皆文件(这种设计方案我们叫VFS,虚拟文件系统)

缓冲区

什么是缓冲区?

缓冲区就是一段内存空间。

这个空间是谁提供的呢?

用户提供的叫用户(层)缓冲区。例如:char buffer[64];scanf(buffer);

为什么要有缓冲区?

主要是为了方便。

一个小例子:2:37:30-

缓冲区存在的意义:提高整机效率。主要是为了提高用户的响应速度

有了缓冲区,缓冲区数据并不会立即写入到文件设备。之前写的进度条。有\n才能显示出来,就是因为缓冲区以及缓冲区的刷新策略决定的。

缓冲区在哪里?

常见的缓冲区刷新策略

1.立即刷新

2.行刷新(行缓冲,即遇到\n、\r)。注意,不是把所有的缓冲区数据全刷出去,而是把包含缓冲区在内的之前的所有数据刷新出去,例如,aaa\n bbb。行刷新会把\n之前的全部刷新了,之后的它不管,等下一个\n到

3.满刷新(全缓冲)

写满之后再刷新

特殊情况:

1.用户强制刷新(用户调用fflush)

2.进程退出

如果我们遇到写了一批数据,进程都退出了,它还没刷新。一般只要进程退出的时候,无论曾经什么刷新策略,只要缓冲区里有数据,这个数据必须得刷新到操作系统内部。

所以缓冲策略也是有特殊情况的。

 以上的read接口是操作系统提供的。

第二十二节2:58:50

我们会发现,加了fork后,为什么变成七条记录了呢?

 这种现象与fork有关

我们发现,打两次的是C语言的接口,打第一次的是操作系统接口。这与缓冲区的刷新策略有关

关于缓冲区的认识:

一般而言,采用行缓冲的设备文件是显示器

采用全缓冲的设备文件是磁盘文件

所有的设备永远都倾向于全缓冲。为什么呢?因为缓冲区满了才刷新,意味着需要更少次IO操作(更少次的外设的访问),缓冲区每次刷新,都需要和外设交互。和外设的交互更少,效率就更高。

和外部设备IO时,数据量的大小不是主要问题,用户和外设预备IO的过程才是最浪费时间的。(我们准备IO是最浪费时间的)

刷新策略是结合具体情况做的妥协。例如:显示器,显示器是直接给用户看的,既要考虑效率,又要照顾用户体验。显示器用行刷新,更符合用户,方便用户。

极端情况下,我们是可以自定义规则的。

上面的函数执行完了,并不代表数据已经刷新了。上面的测试,并不影响系统接口。如果有所谓的缓冲区,我们之前所谈的缓冲区应该是由谁维护的呢?

我们之前说的缓冲区,绝对不是由操作系统提供的。如果是操作系统统一提供,那么我们上面的代码,表现应该是一样的,要么都是5行,要么都是7行。缓冲区它是由C标准库给我们提供的。

缓冲区在哪?

例如,进程调用fputs,实际是通过fputs,把数据写到了C标准库提供的缓冲区里,然后C标准库再定期的把数据刷新到操作系统里。是调用write接口写到操作系统内部的。如果我们是直接使用write接口,我们就是write直接写给操作系统,没有经过缓冲区。

那么到底为什么是一个打印一次,一个打印两次呢?

1.如果是向显示器打印,刷新策略是行刷新,那么最后执行fork的时候,一定是函数执行完了,并且数据已经被刷新了。fork对它已经无意义了。

2.如果我的程序进行了重定向。重定向的本质是本来应该向显示器打印,现在变成了向普通文件打印,这时,其刷新策略隐性的变成了全缓冲。接下来fork的时候,一定是函数执行完了,但是数据还没有刷新(到显示器上)。这些数据在当前进程对应的C标准库中的缓冲区中,这部分数据是属于父进程的数据。所以fork之后,有两个执行流。把数据刷新出去,也是写的过程。结合以上几点我们在退出时会发生一个写时拷贝。第二十三节1:06:00-1:11:00

C标准库给我们提供的叫用户级缓冲区。用户级缓冲区在

C语言中,fopen这个函数对应的返回值是一个FILE*,这个FILE是一个struct file,struct FILE结构体内部封装了fd,除了fd,还包含了该文件fd对应的语言层的缓冲区结构。 

C语言中,打开的FILE我们叫文件流

C++中,cin/cout 是一个类,该类里一定包含了fd(文件描述符),其类里一定定义了buffer

 只要拷贝到内核,该数据就已经属于操作系统了(例如:write一些数据到内核)

第二十三节1:36:00-完

第二十四节

 cc\.cxx\.cpp都是C++文件的后缀

cerr:对应C语言中stderr。

文件系统和iNode,iNode很重要

perror

C语言也是允许同一个函数名+返回值有不同的参数的

vim运行起来就变成进程了

有一个数据结构:位图

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

列宁格勒的街头

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值