3.1.1.应用编程框架介绍
3.1.1.1、什么是应用编程
(1)整个嵌入式linux核心课程包括5个点,按照学习顺序依次是:裸机、C高级、uboot和系统移植、linux应用编程和网络编程、驱动。
(2)典型的嵌入式产品就是基于嵌入式linux操作系统来工作的。典型的嵌入式产品的研发过程就是;第一步让linux系统在硬件上跑起来(系统移植工作),第二步基于linux系统来开发应用程序实现产品功能。
(3)基于linux去做应用编程,其实就是通过调用linux的【系统API】来实现应用需要完成的任务。
3.1.1.2、本课程大纲规划
3.1.1.3、课程设计思路
(1)通过本课程9个小课程的学习,学会如何使用linux系统提供的API(和C库函数)来实现一定的功能,通过学习对应用层的编程有所掌握来配合后面驱动的学习。
(2)如果希望深入学习linux应用尤其是网络编程知识,可以去看一些专门介绍这一块的书。
3.1.1.4、什么是文件IO
(1)IO就是input/output,输入/输出。文件IO的意思就是读写文件。
3.1.2.文件操作的主要接口API
3.1.2.1、什么是【操作系统API】
(1)API(应用程序接口)是一些【函数】,这些函数是由linux操作系统提供支持的,由应用层程序来使用。
(2)应用层程序通过调用API来调用操作系统中的各种功能,来干活。
(3)学习一个操作系统,其实就是学习使用这个操作系统的API。
(比如学习开车的时候,车的内部结构就像是操作系统给予我们的封装,我们开车只要学会怎么踩油门 ,踩刹车等就行,相当于我们操作系统给予我们外部的api接口一样)
(1)今天我们要使用linux系统来读写文件,手段就是学习linux系统API中和文件IO有关的几个。linux有几百个api。
3.1.2.2、linux常用文件IO接口
(1)open、close、write、read、lseek(移动文件指针)
3.1.2.3、文件操作的一般步骤
(1)在linux系统中要操作一个文件,一般是先open打开一个文件,得到一个【文件描述符】,然后对文件进行读写操作(或其他操作),最后close关闭文件即可
(2)强调一点:我们对文件进行操作时,一定要先打开文件,打开成功后才能去操作(如果打开本身失败,后面就不用操作了);最后读写完成之后一定要close关闭文件,否则可能会造成文件损坏。
(3)文件平时是存在【块设备】中的文件系统中的,我们把这种文件叫【静态文件】。当我们去open打开一个文件时,linux内核做的操作包括:内核在进程中建立了一个打开文件的数据结构,记录下我们打开的这个文件;内核在内存中申请一段内存,并且将静态文件的内容从块设备中读取到内存中【特定地址,由操作系统分配】管理存放(叫动态文件)。
小结:静态文件就是平时存储在硬盘中没有被【打开】时候的文件,动态文件就是从块设备中读取到内存中的文件
(4)【打开文件】后,以后对这个文件的读写操作,都是针对内存中这一份动态文件的,而并不是针对静态文件的。当我们对动态文件进行读写后,此时内存中的动态文件和块设备中的静态文件就不同步了,当我们close关闭动态文件时,close内部内核将内存中的动态文件的内容去更新(同步)块设备中的静态文件。(这也就是为什么我们要定期保存的原因,【为了定时更新同步】)
(5)常见的一些现象:
第一个:打开一个大文件时比较慢(因为内核要把【整个文件】从块设备读取到内存中,并且建立数据结构对其进行管理)
第二个:我们写了一半的文件,如果没有点保存直接关机/断电,重启后文件内容丢失。
(6)为什么要这么设计(在内存中为什么要重新搞一份,而不是直接对块设备中的文件进行读写操作)?
因为块设备本身有读写限制(回忆NnadFlash、SD等块设备的读写特征,只能读不能写),本身对块设备进行操作非常不灵活(因为只能按照块来读写)。而内存可以按【字节为单位】来操作,而且可以随机操作(内存就叫RAM,random,可以任意随机指定一个地址去操作),很灵活。所以内核设计文件操作时就这么设计了,增强了文件操作的灵活性。
3.1.2.4、重要概念:文件描述符
(1)文件描述符其实【实质是一个数字】,这个数字在一个进程中表示一个特定的含义,当我们open打开一个文件时,操作系统在内存中构建了一些【数据结构】来表示这个动态文件,然后【返回给应用程序】一个数字作为文件描述符,这个数字就和我们内存中维护这个动态文件的这些数据结构挂钩绑定上了,以后我们应用程序如果要操作这一个动态文件,只需要用这个文件描述符进行区分(这个过程有点类似于我们的一个变量名和变量地址的相互绑定,文件描述符类似于一个文件标号)。
(2)一句话讲清楚文件描述符:文件描述符就是用来区分一个应用程序打开的多个动态文件的。
(3)文件描述符的作用域就是【当前进程】,出了当前进程这个文件描述符就没有意义了,比如我们用notepad++打开多个文件,这个是一个进程,出了这个进程/程序,这个文件描述符就没有意义了。每一个进程就是一个应用程序,不同的应用程序有一套自己的文件管理描述符,含义在不同的作用域范围内也是不同的。
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
文件描述符补充:
1、内核(kernel)利用文件描述符(filedescriptor)来访问文件。文件描述符是非负整数。打开现存文件或新建文件时,内核会返回应用程序一个文件描述符。读写文件也需要使用文件描述符来指定待读写的文件。
2、文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程(应用程序)返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
习惯上,标准输入(standard input)的文件描述符是 0,标准输出(standard output)是 1,标准错误(standard error)是2, 尽管这种习惯并非Unix内核的特性,但是因为一些 shell 和很多应用程序都使用这种习惯,因此,如果内核不遵循这种习惯的话,很多应用程序将不能使用。
POSIX 定义了 STDIN_FILENO、STDOUT_FILENO 和 STDERR_FILENO 来代替 0、1、2。这三个符号常量的定义位于头文件 unistd.h。
文件描述符的有效范围是 0 到 OPEN_MAX。一般来说,每个进程最多可以打开 64 个文件(0 — 63)。对于 FreeBSD Mac OS X 10.3 和 Solaris 9 来说,每个进程最多可以打开文件的多少取决于系统内存的大小,int 的大小,以及系统管理员设定的限制。Linux 2.4.22 强制规定最多不能超过 1,048,576 。
文件描述符是由无符号整数表示的句柄,进程使用它来标识打开的文件。文件描述符与包括相关信息(如文件的打开模式、文件的位置类型、文件的初始类型等)的文件对象(描述一个文件的信息的数据结构)相关联,这些信息被称作文件的上下文。
3、进程(应用程序)获取文件描述符最常见的方法是通过本机子例程open或create获取或者通过从父进程继承。后一种方法允许子进程同样能够访问由父进程使用的文件。文件描述符对于每个进程一般是唯一的。当用fork子例程创建某个子进程时,该子进程会获得其父进程所有文件描述符的副本,这些文件描述符在执行fork时打开。在由fcntl、dup和dup2子例程复制或拷贝某个进程时,会发生同样的复制过程。
【对于每个进程操作系统内核在u_block结构中维护文件描述符表,所有的文件描述符都在该表中建立索引】
4、优点
文件描述符的好处主要有两个:
基于文件描述符的I/O操作兼容POSIX标准。
在UNIX、Linux的系统调用中,大量的系统调用都是依赖于文件描述符。
例如,下面的代码就示范了如何基于文件描述符来读取当前目录下的一个指定文件,并把文件内容打印至Console中。
此外,在Linux系列的操作系统上,由于Linux的设计思想便是把一切设备都视作文件。因此,文件描述符为在该系列平台上进行设备相关的编程实际上提供了一个统一的方法。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(void)
{
int fd;
int numbytes;
char path[] = "file";
char buf[256];
/*
* O_CREAT: 如果文件不存在则创建
* O_RDONLY:以只读模式打开文件
*/
fd = open(path, O_CREAT | O_RDONLY, 0644);
if(fd < 0)
{
perror("open()");
exit(EXIT_FAILURE);
}
memset(buf, 0x00, 256);
while((numbytes = read(fd, buf, 255)) > 0)
{
printf("%d bytes read: %s", numbytes, buf);
memset(buf, 0x00, 256);
}
close(fd);
exit(EXIT_SUCCESS);
}
缺点
文件描述符的概念存在两大缺点:
在非UNIX/Linux操作系统上(如Windows NT),无法基于这一概念进行编程。
由于文件描述符在形式上不过是个整数,当代码量增大时,会使编程者难以分清哪些整数意味着数据,哪些意味着文件描述符。因此,完成的代码可读性也就会变得很差。
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
3.1.3.一个简单的文件读写实例
3.1.3.1、打开文件与关闭文件
(1)linux中的文件描述符fd的合法范围是0或者一个正整数,不可能是一个负数。
(2)open返回的fd程序必须记录好,以后向这个文件的所有操作都要靠这个fd去对应这个文件,最后关闭文件时也需要fd去指定关闭这个文件。如果在我们关闭文件前fd丢掉了那就惨了,这个文件没法关闭了也没法读写了。
输入型参数和输出型参数:
输入型参数是指这个参数的值已知,由外面传给函数里使用.
输出型参数是指这个参数的值未知,要通过函数传出来.
如拷贝函数char *strcpy( char *strDestination, const char *strSource );
函数功能是把字符串strSource 拷给strDestination
这里strSource 是输入型形参,strDestination是输出型形参.
void main()
{
char *strSrc = "abcd";
char strDes[10];
strcpy(strDes,strSrc);//strDes未知,要通过调用函数后才能得到,strSrc已知,传递给函数使用.
}
3.1.3.2、实时查man手册
(1)当我们写应用程序时,很多API原型都不可能记得,所以要实时查询,用man手册
(2)man 1 xx查linux shell命令,man 2 xxx查linux系统API, man 3 xxx查C库函数
(3)包含必要的系统头文件
man 2 open
man 2 close
man 2 read
man 2 write
3.1.3.3、读取文件内容
(1)ssize_t read(int fd, void *buf, size_t count); //void *buf是一个输出型参数,被写
fd表示要读取哪个文件,fd一般由前面的open返回得到
buf是应用程序自己提供的一段内存缓冲区,用来【存储】读出的内容
count是我们要读取的字节数
返回值ssize_t类型是linux内核用typedef重定义的一个类型(其实就是int,为了构建平台可移植性),返回值表示真正成功读取的字节数。
在文件所有范围内可以读取任意个字节。
【read从参数fd指定的文件中读取数据到大小为count的缓存buf中,然后返回读取的实际读取到的字节数】
3.1.3.4、向文件中写入
ssize_t write(int fd,const void *buf, size_t count);//const void *buf是一个输入型参数,里面的内容只能够被读,size_t count是一个要从缓冲区中读出来的字节数,ssize_t代表写成功了多少个字节
【write函数向参数fd指定的文件从缓存buf中拿出count个字节到文件中,返回值为实际写入的字节数】
(1)写入用write系统调用,write的原型和理解方法和read相似
(2)注意const在buf前面的作用,结合C语言高级专题中的输入型参数和输出型参数一节来理解。
(3)注意buf的指针类型为void,结合C语言高级专题中void类型(任意类型)含义的讲解
(4)刚才先写入12字节,然后读出结果读出是0(但是读出成功了),这个问题的答案后面章节会讲,大家先思考一下。
3.1.4.open函数的flag详解1
3.1.4.1、打开文件时对文件读写权限的要求:O_RDONLY (只读方式) O_WRONLY (只写方式) O_RDWR(可读可写)
(1)linux中文件有读写权限,我们在open打开文件时也可以附带一定的权限说明(譬如O_RDONLY就表示以只读方式打开,O_WRONLY表示以只写方式打开,O_RDWR表示以可读可写方式打开)
(2)当我们附带了权限后,打开的文件就只能按照这种权限来操作。
(3)我们很少使用只写的方式。
0
3.1.4.2、打开【存在】并有内容的文件时:O_APPEND、O_TRUNC
(1)思考一个问题:当我们【打开】一个已经存在并且内部有内容的文件时会怎么样?
可能结果1:新内容会替代原来的内容(原来的内容就不见了,丢了或者说是覆盖掉了)
可能结果2:新内容添加在前面,原来的内容继续在后面
可能结果3:新内容附加在后面,原来的内容还在前面
可能结果4:不读不写的时候,原来的文件中的内容保持不变
/*
在我们操作文件的时候,可以给这个文件的操作指定多个操作属性,这些属性之间用‘|’位或符号相连接起来。eg: fd=open("test.txt",O_RDWR | O_APPEND);
*/
(2)O_TRUNC属性去打开文件(注意这里只是说打开一个文件,还没有说是读还是写这个文件,仅仅在打开的时候就会把文件里有的内容丢弃 )时,如果这个文件中本来是有内容的,则原来的内容会被丢弃。这就对应上面的结果1
(3)O_APPEND属性去打开文件时,如果这个文件中本来是有内容的,则新写入的内容会接续到原来内容的后面,对应结果3
(4)默认不使用O_APPEND和O_TRUNC属性时就是结果4:原来的文件中的内容保持不变。
(5)如果O_APPEND和O_TRUNC同时出现会怎么样?O_TRUNC起作用,O_APPEND这时候就不起作用了。
3.1.4.3、exit、_exit、_Exit退出进程
(1)当我们程序在前面步骤操作失败导致后面的操作都没有可能进行下去时,应该在前面的错误监测中结束整个程序,不应该继续让程序运行下去了。
(2)我们如何退出程序?
第一种;在main用return,一般原则是程序正常终止return 0,如果程序异常终止则return -1。
第二种:正式终止进程(程序)应该使用exit或者_exit或者_Exit之一。(具体用哪个要包含哪一个对应的头文件,具体可以查man手册)
3.1.5.open函数的flag详解2
3.1.5.1、打开【不存在】的文件时:O_CREAT、O_EXCL
(1)思考:当我们去打开一个并不存在的文件时会怎样?当我们open打开一个文件时如果这个文件名不存在则会打开文件错误。
(2)vi或者windows下的notepad++,都可以直接打开一个尚未存在的文件。【有点类似于创建了一个文件】
(3)open的flag O_CREAT就是为了应对这种打开一个并不存在的文件的。O_CREAT就表示我们当前打开的文件并不存在,我们就要去创建并且打开它。
(4)思考:当我们open使用了O_CREAT,但是文件已经存在的情况下会怎样?
(5)结论:open中加入O_CREAT后,不管原来这个文件存在与否都能打开成功,如果原来这个文件不存在则创建一个【空的新文件】,如果原来这个文件存在则会重新创建这个文件,原来的内容会被消除掉(有点类似于先删除原来的文件再创建一个新的)
(6)这样可能带来一个问题?我们本来是想去创建一个新文件的,但是把文件名搞错了弄成了一个老文件名,结果老文件就被意外修改了。我们希望的效果是:如果我CREAT要创建的是一个已经存在的名字的文件,则给我报错,不要去创建。
(7)这个效果就要靠O_EXCL标志和O_CREAT标志来结合使用。当这连个标志一起的时候,则没有文件时创建文件,有这个文件时会报错提醒我们已经有这个文件存在了。
(8)open函数在使用O_CREAT标志去创建文件时,可以使用第三个参数mode来指定要创建的文件的权限(可读或者可写)。mode使用4个数字来指定权限的,其中后面三个很重要,对应我们要创建的这个文件的权限标志。譬如一般创建一个可读可写不可执行的文件就用0666 比如:fd=open("tst.txt",O_RDWR | O_CREAT,0666);mode位就是和创建文件相互配套使用的。
3.1.5.2、O_NONBLOCK
(1)阻塞与非阻塞。如果一个函数是阻塞式的,则我们调用这个函数时当前进程有可能被卡住(阻塞住,实质是这个函数内部要完成的事情条件不具备,当前没法做,要等待条件成熟,但是最终还是要执行,类似于银行的排队等待,不管前面有多少人排队,我就要等待),【函数被阻塞住了就不能立刻返回】;如果一个函数是非阻塞式的那么我们调用这个函数后一定会立即返回,但是函数有没有完成指定的任务不一定(有可能排队的时候前面没人直接完成任务,也有可能是排队的人太多了,直接回来了)。
小结:阻塞式一定能返回一个完成的结果,但是时间是不确定的;而非阻塞式的能够立即返回一个结果,但是这个结果不一定执行成功。
(2)阻塞和非阻塞是两种不同的设计思路,并没有好坏。总的来说,【阻塞式的结果有保障(一定会去做的)但是时间没保障;非阻塞式的时间有保障但是结果没保障(做不做不一定的)。】
(3)操作系统提供的API和由API封装而成的库函数,有很多本身就是被设计为阻塞式或者非阻塞式的,所以我们应用程度调用这些函数的时候心里得非常清楚。
(4)我们打开一个文件默认就是阻塞式的,如果你希望以非阻塞的方式打开文件,则open的flag中要加O_NONBLOCK标志。
(2)只用于设备文件(硬件器件之类,比如串口文件),而不用于普通文件。
3.1.5.3、O_SYNC
(1)write阻塞等待底层完成写入才返回到应用层。
(2)无O_SYNC时write只是将内容写入【底层缓冲区】即可返回,然后底层(操作系统中负责实现open、write这些操作的那些代码,也包含OS中读写硬盘等底层硬件的代码)【在合适的时候会】将buf中的内容一次性的同步到硬盘中。这种设计是为了提升硬件操作的性能和销量,提升硬件寿命,避免反复操作硬盘硬件;但是有时候我们希望硬件不要等待,而是直接将我们的内容写入硬盘中,这时候就可以在open的flag中用O_SYNC标志。
3.1.6.文件读写的一些细节
3.1.6.1、errno和perror
(1)errno就是error number,意思就是错误号码。可以看作是linux系统中维护的一个变量,linux系统中对各种【常见错误】做了个编号,当函数执行错误时,函数会返回一个特定的errno编号来告诉我们这个函数到底哪里错了,或者说是属于哪一种错误(比如内存溢出,函数执行错误等)。 【类似于一个错误编号表。】
(2)errno是由OS来维护的一个【全局变量】,任何OS内部函数都可以通过设置errno来告诉上层调用者究竟刚才发生了一个什么错误。
(3)errno本身实质是一个int类型的数字,每个数字编号对应一种错误。当我们只看errno时只能得到一个错误编号数字(譬如-37),不适应于人看。 【有点类似于中断中的中断编号,也有点类似于一个数组】
解决方案就是linux系统提供了一个函数perror(意思print error),perror函数【内部,(不用给这个函数传参)】会读取errno并且将这个 成对应的错误信息字符串,然后print打印出来。 【eg:perror ("open: ");】
man 3 perror
3.1.6.2、read和write的count
(1)count和返回值的关系。count参数表示我们想要写或者读的字节数,返回值表示【实际完成的】要写或者读的字节数。实现的有可能等于想要读写的,也有可能小于(说明没完成任务)
(2)count再和阻塞非阻塞结合起来,就会更加复杂。如果一个函数是阻塞式的,则我们要读取30个,结果暂时只有20个时就会被阻塞住,等待剩余的10个可以读。【比如在阻塞式的读取的时候,文件只有50个字节,我们设置的count为大于50个字节,则程序就会阻塞住,因为它想要读取完你设定的值,交给的任务】
(3)有时候我们写正式程序时,我们要读取或者写入的是一个很庞大的文件(譬如文件有2MB),我们不可能把count设置为2*1024*1024,而应该去把count设置为一个合适的数字(譬如2048、4096),然后通过【多次循环读取】来实现全部读完。
3.1.6.3、文件IO效率和标准IO
(1)文件IO就指的是我们当前在讲的【open、close、write、read】等API函数构成的一套用来读写文件的体系,这套体系可以很好的完成文件读写,但是效率并不是最高的。
(2)应用层C语言库函数提供了一些用来做文件读写的函数列表,叫标准IO。标准IO由一系列的C库函数构成(fopen、fclose、fwrite、fread),加一个f代表是库函数,这些标准IO函数其实是由【系统提供的文件IOAPI】封装而来的(fopen内部其实调用的还是open,fwrite内部还是通过write来完成文件写入的)。标准IO加了封装之后主要是为了在应用层添加一个缓冲机制,这样我们通过fwrite写入的内容不是直接进入内核中的buf,而是先进入应用层标准IO库自己维护的buf中,然后标准IO库自己根据操作系统单次write的最佳count来选择好的时机来完成write到内核中的buf(内核中的buf再根据硬盘的特性来选择好的时机去最终写入硬盘中)。
---------------------------------------------------------------
3.1.7.linux系统如何管理文件
3.1.7.1、硬盘中的静态文件和inode(i节点)
(1)文件平时都在存放在硬盘中的,硬盘中存储的文件以一种固定的形式存放的,我们叫静态文件。
硬盘属于一种块设备,这些块设备中又有一个个的扇区,一个扇区一般的是512个字节,多个扇区(64或者32)又组成一个块。整个设备里可能有好多个块,比如一个设备里有10000个块,一个块里有300个扇区,一个扇区有512个字节,则这个设备的总存储容量就是10000*300*512 B。
文件为什么一经过压缩就会变小呢?因为压缩后可以把在磁盘上不同位置存储的不同文件放到一个相同的文件夹中,这个压缩包里集中了各种各样的文件。
(2)一块硬盘中可以分为两大区域:一个是硬盘内容存储管理表项,另一个是真正存储内容的区域。操作系统访问硬盘时是先去读取硬盘内容管理表(作为搜找一个文件存储路径的索引),从中找到我们要访问的那个文件的扇区级别的信息,然后再通过这个信息去查询真正存储内容的区域,最后得到我们要的文件。
(3)操作系统最初拿到的信息是文件名,最终得到的是文件内容。第一步就是去查询硬盘内容管理表,这个管理表中以文件为单位记录了各个文件的各种信息,每一个文件有一个信息列表数据结构(我们叫inode结构体,i节点,其实质是一个结构体,这个结构体有很多元素,每个元素记录了这个文件的一些信息,其中就包括文件名、文件在硬盘上对应的扇区号、块号那些东西·····)
强调:硬盘管理的时候是以【文件】为基本单位的,每个文件一个inode结构体,每个inode有一个数字编号,对应一个结构体,结构体中记录了各种信息。(我们操作系统拿到一个文件名后就会在硬盘内容管理表中利用文件名循环匹配硬盘内容管理表中的文件信息节点inode,怎么匹配呢?我觉得应该是拿文件名去匹配节点里的一个结构体指向元素)
小结:i节点就是我们操作系统中用来记录文件各种信息(存储、大小之类)的一种数据结构,这种数据结构能够让我们去管理硬盘上的存储文件。
(4)联系平时实践,大家格式化硬盘(U盘)时发现有:快速格式化和底层格式化。快速格式化非常快,格式化一个32GB的U盘只要1秒钟,普通格式化格式化速度慢。这两个的差异?其实快速格式化就是只删除了U盘中的硬盘内容管理表(其实就是inode),真正存储的内容没有动。这种格式化的内容是有可能被找回的(我们可以重新扫描各个扇区,然后对各个扇区的内容文件进行重新的管理组建,形成一个新的内容管理表)。
如图所示:
3.1.7.2、内存中被打开的文件和vnode(v节点)
(1)一个程序的运行就是一个进程,我们在该程序中打开的文件就属于某个进程。每个进程都有一个数据结构用来记录这个进程的所有信息(叫进程信息表),进程信息表中有一个指针,该指针会指向一个文件管理表,文件管理表中记录了当前进程打开的所有文件及其相关信息。文件管理表中用来索引各个打开的文件的index就是文件描述符fd,我们最终找到的就是一个已经被打开的文件的管理结构体vnode(也就是说vnode就是专门用来管理已经被打开的文件的)
(2)一个vnode中就记录了一个被打开的文件的各种信息(比如文件大小,文件在【内存】中的位置),而且我们只要知道这个文件的fd,就可以很容易的找到这个文件的vnode进而对这个文件进行各种操作。
小结:在我们的动态文件中,我们的内存中有一个【所有的进程管理表】,我们打开一个文件的时候就要去这个进程管理表中找到对应的一个进程,这个进程里有一个该进程的信息管理表,其实是一个结构体,这个结构体中其中有一个元素指针指向保存着所有的打开的文件信息的表,叫做【文件管理表】,文件管理表通过【文件描述符fd】来找到某一个具体打开的文件,对于一个具体打开的文件,该文件的各种信息(在内存中存储位置、存储大小等)由【vnode管理】,vnode就是一个结构体,里面有文件指针这个元素,这个指针表示当前我们正在操作文件流的哪个位置。
如图所示:
3.1.7.3、文件与【流】的概念
(1)流(stream)对应自然界的水流。文件操作中,文件类似是一个大包裹,里面装了一堆字符,但是文件被读出/写入时都只能一个字符一个字符的进行,而不能一股脑儿的读写,那么一个文件中N多的个字符被挨个一次读出/写入时,这些字符就构成了一个字符流。
(2)流这个概念是动态的,不是静态的。
(3)编程中提到流这个概念,一般都是IO相关的。所以经常叫IO流。文件操作时就构成了一个IO流。
---------------------------------------------------------------------
3.1.8.lseek详解
3.1.8.1、lseek函数介绍
(1)文件指针:当我们要对一个文件进行读写时,一定需要先打开这个文件,所以我们读写的所有文件都是动态文件。动态文件在内存中的形态就是文件流的形式。
(2)文件流很长,里面有很多个字节。那我们当前正在操作的是哪个位置?GUI模式下的软件用光标来标识这个当前正在操作的位置,这是给人看的。【光标就好像在我们的这个流里面打了一个截点,光标就是在我们的这个流里来回的动的】
(3)在内存里的动态文件中,我们会通过文件指针来表征这个正在操作的位置。所谓文件指针,就是我们文件信息管理表vnode这个结构体里面的一个指针。【所以文件指针其实是vnode中的一个元素】这个指针表示当前我们正在操作文件流的哪个位置。这个指针不能被直接访问,linux系统用lseek函数来访问这个文件指针。
(4)当我们打开一个空文件时,默认情况下文件指针指向文件流的开始。所以这时候去write时写入就是从文件开头位置开始的。write和read函数本身自带移动文件指针的功能,所以当我write了n个字节后,【文件指针会自动依次向后移动n位。】如果需要人为的随意更改文件指针(让光标随意移动),自由化,那就只能通过lseek函数了。
(5)read和write函数都是从【当前文件指针处】开始操作的,所以当我们用lseek显式的将文件指针移动后,那么再去read/write时就是从【移动过后的位置】开始的。但是文件中的内容还是完整存在的。
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);//fd就是代表当前文件,whence就是代表一个参考位置(用来表示文件开始处到文件当前位置的字节数)offset代表一个偏移量。也就是说从参考位置开始偏移了多少位置。off_t代表返回的是实际位置相对于开头偏移的字节数(也就是最终位置在哪)。
函数原型
#include<unistd.h>
off_t lseek(int fildes,off_t offset ,int whence);
参数 offset 的含义取决于参数 whence:
1. 如果 whence 是 SEEK_SET,则返回的文件偏移量将被设置为 offset。
2. 如果 whence 是 SEEK_CUR,则返回的文件偏移量将被设置为 cfo 加上 offset,
offset 可以为正也可以为负。
3. 如果 whence 是 SEEK_END,则返回的文件偏移量将被设置为文件长度加上 offset,
offset 可以为正也可以为负。
1) 欲将读写位置移到文件开头时:
lseek(int fildes,0,SEEK_SET);
2) 欲将读写位置移到文件尾时:
lseek(int fildes,0,SEEK_END);
3) 想要取得目前文件位置时:
lseek(int fildes,0,SEEK_CUR);
返回值:
当调用成功时则返回目前的【读写位置】,也就是距离文件开头多少个字节。若有错误则返回-1,errno 会存放错误代码。
(6)回顾前面一节中我们从空文件,先write写了12字节,然后read时是空的(但是此时我们打开文件后发现12字节确实写进来了,这个的原因就是把文件指针放到了文件末尾)。
3.1.8.2、用lseek计算文件长度
(1)linux中并没有一个函数可以直接返回一个文件的长度(因为不需要)。但是我们做项目时经常会需要知道一个文件的长度,怎么办?【自己利用lseek来写一个函数得到文件长度即可。】
原理就是当我们新打开一个文件时,此时文件指针在文件最开头处,我们用lseek函数把文件指针移动到文件末尾处,然后返回值就是我们文件的末尾指针距离文件开头的偏移量,即文件的长度。
eg:return=lseek(fd,0,SEEK_END);//表示从文件末尾
3.1.8.3、用lseek构建空洞文件
(1)空洞文件就是这个文件中有一段是空的。
(2)普通文件中间是不能有空的,因为我们write时文件指针是依次从前到后去移动的,不可能绕过前面直接到后面。
(3)我们打开一个文件后,用lseek往后跳过一段,再write写入一段,就会构成一个空洞文件(这个空洞文件就是有一段没有内容)。
(4)空洞文件方法对多线程共同操作文件是及其有用的。有时候我们创建一个很大的文件(比如视频文件),如果从头开始依次构建时间很长。有一种思路就是将文件分为多段,然后多线程来操作每个线程负责其中一段的写入。
【就像修100公里的高速公路,分成20个段来修,每个段就只负责5公里,就可以大大提高效率】
(5)空洞文件就是由lseek来构建的。
空洞文件构建简单示例:
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<unistd.h>
#define FILEPATH "./a.txt"
void main(int argc,char *argv[])
{
char a[50]="abcdefg";
char b[50]="znm";
int fd=-1;
int k;
int i,j;
fd=open(FILEPATH,O_RDWR);
k=write(fd,&a,5);
i= lseek(fd,5,SEEK_CUR);
j=write(fd,&b,3);
close(fd);
}
3.1.9.多次打开同一文件与O_APPEND
3.1.9.1、重复打开同一文件读取
(1)一个进程中两次打开同一个文件,然后分别读取,看结果会怎么样
(2)结果无非2种情况:一种是fd1和fd2分别读,第二种是接续读。经过实验验证,证明了结果是fd1和fd2分别读。
(3)分别读说明:我们使用open两次打开同一个文件时,fd1和fd2所对应的文件指针是不同的2个独立的指针。文件指针是包含在动态文件的文件管理表中的,所以可以看出linux系统的进程中不同fd对应的是不同的独立的文件管理表。
3.1.9.2、重复打开同一文件写入
(1)一个进程中2个打开同一个文件,得到fd1和fd2.然后看是分别写还是接续写?
(2)正常情况下我们有时候需要分别写,有时候又需要接续写,所以这两种本身是没有好坏之分的。关键看用户需求
(3)默认情况下应该是:分别写(实验验证过的)
3.1.9.3、加O_APPEND解决覆盖问题
(1)有时候我们希望接续写而不是分别写?办法就是在open时加O_APPEND标志即可
3.1.9.4、O_APPEND的实现原理和其原子操作性说明
(1)O_APPEND为什么能够将分别写改为接续写?关键的核心的东西是文件指针。分别写的内部原理就是2个fd拥有不同的文件指针,并且彼此只考虑自己的位移。但是O_APPEND标志可以让write和read函数内部多做一件事情,就是移动自己的文件指针的同时也去把别人的文件指针同时移动。(也就是说即使加了O_APPEND,fd1和fd2还是各自拥有一个独立的文件指针,但是这两个文件指针关联起来了,一个动了会通知另一个跟着动)
(2)O_APPEND对文件指针的影响,对文件的读写是原子的。
(3)原子操作的含义是:整个操作一旦开始是不会被打断的,必须直到操作结束其他代码才能得以调度运行,这就叫原子操作。每种操作系统中都有一些机制来实现原子操作,以保证那些需要原子操作的任务可以运行。比如一个程序的执行过程不能被cpu的多个进程同时执行的过程打断。
代码示例:
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<unistd.h>
#define FILEPATH "./a.txt"
void main(int argc,char *argv[])
{
char a[50]="aa";
char b[50]="bb";
int fd1=-1;
int fd2=-1;
int k;
int i,j,l;
//进程一
fd1=open(FILEPATH,O_RDWR | O_APPEND);
j=write(fd1,&a,2);
k=close(fd1);
//进程二
fd2=open(FILEPATH,O_RDWR | O_APPEND);
l=write(fd2,&b,2);
i=close(fd2);
}
--------------------------------------------------------
3.1.10.文件共享的实现方式
3.1.10.1、什么是文件共享
(1)文件共享就是同一个文件(同一个文件指的是同一个inode,同一个pathname)被多个独立的读写体(几乎可以理解为多个文件描述符)去同时(一个打开尚未关闭的同时另一个去操作)操作。
(2)文件共享的意义有很多:譬如我们可以通过文件共享来实现多线程同时操作同一个大文件,以减少文件读写时间,提升效率。
3.1.10.2、文件共享的3种实现方式
(1)文件共享的核心就是怎么弄出来多个文件描述符指向同一个文件。
(2)常见的有3种文件共享的情况:第一种是同一个进程中多次使用open打开同一个文件,第二种是在不同进程中去分别使用open打开同一个文件(这时候因为两个fd在不同的进程中,所以两个fd的数字可以相同也可以不同),第三种情况是后面要学的,linux系统提供了dup和dup2两个API来让进程复制文件描述符。
(3)我们分析文件共享时的核心关注点在于:分别写/读还是接续写/读
3.1.10.3、再论文件描述符
(1)文件描述符的本质是一个数字,这个数字本质上是进程表中文件描述符表的一个表项,进程通过文件描述符作为index去索引查表得到文件表指针,再间接访问得到这个文件对应的文件表。
(2)文件描述符这个数字是open系统调用内部由操作系统自动分配的,操作系统分配这个fd时也不是随意分配,也是遵照一定的规律的,我们现在就要研究这个规律。
(3)操作系统规定,fd从0开始依次增加。fd也是有最大限制的,在linux的早期版本中(0.11)fd最大是20,所以当时一个进程最多允许打开20个文件。linux中文件描述符表是个数组(不是链表),所以这个文件描述符表其实就是一个数组,fd是index,文件表指针是value
(4)当我们去open时,内核会从文件描述符表中挑选一个最小的未被使用的数字给我们返回。也就是说如果之前fd已经占满了0-9,那么我们下次open得到的一定是10.(但是如果上一个fd得到的是9,下一个不一定是10,这是因为可能前面更小的一个fd已经被close释放掉了)
(5)fd中0、1、2已经默认被系统占用了,因此用户进程得到的最小的fd就是3了。
(6)linux内核占用了0、1、2这三个fd是有用的,当我们运行一个程序得到一个进程时,内部就默认已经打开了3个文件,这三个文件对应的fd就是0、1、2。这三个文件分别叫stdin、stdout、stderr。也就是标准输入、标准输出、标准错误,因此用户进程得到的最小的fd就是3了。
(7)标准输入一般对应的是键盘(可以理解为:0这个fd对应的是键盘的设备文件),标准输出一般是LCD显示器(可以理解为:1对应LCD的设备文件)
(8)printf函数其实就是默认输出到标准输出stdout上了。stdio中还有一个函数叫fpirntf,这个函数就可以指定输出到哪个文件描述符中。
3.1.11. 文件描述符的复制1
3.1.11.1、dup和dup2函数介绍
3.1.11.2、使用dup进行文件描述符复制
(1)dup系统调用对fd进行复制,会返回一个新的文件描述符(譬如原来的fd是3,返回的就是4)
(2) dup系统调用有一个特点,就是自己不能指定复制后得到的fd的数字是多少,而是由操作系统内部自动分配的,分配的原则遵守操作系统分配fd的原则。
(3)dup返回的fd和原来的oldfd都指向oldfd打开的那个【动态文件】,操作这两个fd实际操作的都是oldfd打开的那个文件。实际上构成了文件共享。
(4)dup返回的fd和原来的oldfd同时向一个文件写入时,结果是分别写还是接续写? 答案是接续性写,因为是同一个文件内。
3.1.11.3、使用dup的缺陷分析
(1)dup并不能指定分配的新的文件描述符的数字,dup2系统调用修复了这个缺陷,所以平时项目中实际使用时根据具体情况来决定用dup还是dup2.
3.1.11.4、练习
(1)之前课程讲过0、1、2这三个fd被标准输入、输出、错误通道占用。而且我们可以关闭这三个
(2)我们可以close(1)关闭标准输出,关闭后我们printf输出到标准输出的内容就看不到了
(3)然后我们可以使用dup重新分配得到1这个fd,这时候就把oldfd打开的这个文件和我们1这个标准输出通道给绑定起来了。这就叫标准输出的重定位。
(4)可以看出,我们可以使用close和dup配合进行文件的重定位。
代码示例1:
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<unistd.h>
#define FILEPATH "./a.txt"
void main(int argc,char *argv[])
{
int fd=-1;
int i=-1; //实际向文件中写入的字节数
int j=-1;//实际向文件中写入的字节数
int k;//用dup复制后的文件描述符
char a[100]="aa";
char b[100]= "bb";
//打开一个文件操作
fd=open(FILEPATH, O_RDWR );
if(-1==fd)
{
printf("文件打开失败!\n");
perror("open:");
_exit(-1);
}
else
{
printf("文件打开成功!fd=%d\n",fd);
}
k=dup(fd);
while(1)
{
//写一个文件操作
i=write(fd,&a,2);
if(-1==i)
{
printf("写入文件失败\n");
_exit(-1);
}
//写一个文件操作
j=write(k,&b,2);
if(-1==j)
{
printf("写入文件失败\n");
perror("写入");
_exit(-1);
}
}
close(fd);
}
代码示例2:
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<unistd.h>
#define FILEPATH "1.txt"
void main(int argc,char *argv[])
{
int fd=-1;
int i=-1; //实际向文件中写入的字节数
int j=-1;//实际向文件中写入的字节数
int k;//用dup复制后的文件描述符
char b[100]= "bbbbbadfghgdfh";
//打开一个文件操作
fd=open(FILEPATH, O_RDWR );
if(-1==fd)
{
printf("文件打开失败!\n");
perror("open:");
_exit(-1);
}
else
{
printf("文件打开成功!fd=%d\n",fd);
}
close(1); //关闭标准输出文件描述符1
k=dup(fd); //实现重定位
j=write(k,&b,20);
if(-1==j)
{
printf("写入文件失败\n");
perror("写入");
_exit(-1);
}
close(fd);
}
3.1.12.文件描述符的复制2
3.1.12.1、使用dup2进行文件描述符复制
(1)dup2和dup的作用是一样的,都是复制一个新的文件描述符。但是dup2允许用户指定新的文件描述符的数字。
(2)使用方法看man手册函数原型即可。
int dup2(int oldfd, int newfd);
如果成功则返回一个我们指定的文件描述符,反之出错返回-1.
3.1.12.2、dup2共享文件交叉写入测试
(1)dup2复制的文件描述符,和原来的文件描述符虽然数字不一样,但是这两个个指向同一个打开的文件
(2)交叉写入的时候,结果是接续写(实验证明的)。
代码示例:
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<unistd.h>
#define FILEPATH "./a.txt"
void main(int argc,char *argv[])
{
int fd=-1;
int i=-1; //实际向文件中写入的字节数
int j=-1;//实际向文件中写入的字节数
int k;//用dup复制后的文件描述符
char a[100]="aa";
char b[100]= "bb";
//打开一个文件操作
fd=open(FILEPATH, O_RDWR );
if(-1==fd)
{
printf("文件打开失败!\n");
perror("open:");
_exit(-1);
}
else
{
printf("文件打开成功!fd=%d\n",fd);
}
k=dup2(fd,5); //调用dup2函数
printf("k=%d\n",k);
while(1)
{
//第一次写一个文件操作
i=write(fd,&a,2);
if(-1==i)
{
printf("写入文件失败\n");
_exit(-1);
}
//第二次写一个文件操作
j=write(k,&b,2);
if(-1==j)
{
printf("写入文件失败\n");
perror("写入");
_exit(-1);
}
}
close(fd);
}
3.1.12.3、命令行中重定位命令 >
这个命令的主要作用就是在终端输出的东西重定位到一个文件中来输出。
(1)linux中的shell命令执行后,打印结果都是默认进入stdout标准输出的(本质上是因为这些命令譬如ls、pwd等都是调用printf进行打印的),所以我们可以在linux的终端shell中直接看到命令执行的结果。
(2)能否想办法把ls、pwd等命令的输出给重定位到一个文件中(譬如2.txt)去,实际上linux终端支持一个重定位的符号>很简单可以做到这点。(eg: ls > 2.txt)
(3)这个>的实现原理,其实就是利用open+close+dup,open打开一个文件2.txt,然后close(1)关闭stdout,然后dup将1和2.txt文件关联起来即可。
3.1.13.fcntl函数介绍
3.1.13.1、fcntl的原型和作用
(1)fcntl函数是一个多功能文件管理(根据文件描述符来操作文件的特性)的工具箱,接收2个参数+1个变参。第一个参数是fd表示要操作哪个文件,第二个参数是cmd表示要进行哪个命令操作。变参是用来传递参数的,要配合cmd来使用(供命令使用的参数)。
用法:
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);
参数:
fd:文件描述词。
cmd:操作命令。
arg:供命令使用的参数。
lock:同上。
(2)cmd的样子类似于F_XXX,不同的cmd具有不同的功能。学习时没必要去把所有的cmd的含义都弄清楚(也记不住),只需要弄明白一个作为案例,搞清楚它怎么看怎么用就行了,其他的是类似的。其他的当我们在使用中碰到了一个fcntl的不认识的cmd时再去查man手册即可。
3.1.13.2、fcntl的常用cmd
(1)F_DUPFD这个cmd的作用是复制文件描述符(作用类似于dup和dup2),这个命令的功能是从可用的fd数字列表中找一个比arg大或者和arg一样大的数字作为oldfd的一个复制的fd,和dup2有点像但是不同。dup2返回的就是我们指定的那个newfd否则就会出错,返回-1;但是F_DUPFD命令返回的是>=arg的最小的那一个数字。
3.1.13.3、使用fcntl模拟dup2
3.1.14.标准IO库介绍
3.1.14.1、标准IO和文件IO有什么区别
(1)看起来使用时都是函数,但是:标准IO是C库函数,而文件IO是linux系统的API,API类似于一种接口,是由操作系统提供的。
(2)C语言库函数是【由API封装】而来的。C库函数内部也是通过调用API来完成操作的,但是【库函数因为多了一层封装】,所以比API要更加好用一些。
(3)库函数比API还有一个优势就是:API在不同的操作系统之间是不能通用的,但是C库函数在不同操作系统中几乎是一样的。【所以C库函数具有可移植性而API不具有可移植性。
(4)性能上和易用性上看,C库函数一般要好一些。譬如IO,文件IO是不带缓存的,而标准IO是带缓存的,等到合适的时间,我们的操作系统才会把写在缓冲区里的内容真正搞到下一层去。因此标准IO比文件IO性能要更高。
3.1.14.2、常用标准IO函数介绍
(1)常见的标准IO库函数有:fopen、fclose、fwrite、fread、ffulsh(刷新标准库函数的缓存,直接写进操作系统的缓冲区中)、fseek
3.1.14.3、一个简单的标准IO读写文件实例
FILE *fopen(const char *path, const char *mode);
代码示例:
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<unistd.h>
#define FILEPATH ".//a.txt"
void main(int argc,char *argv[])
{
FILE *file; //定义一个文件指针
int k;//用fclose函数后返回的值,若返回值不为0则表示关闭失败
file=fopen(FILEPATH, "r+");
if(NULL!= file)
{
printf("文件打开成功!\n");
}
else
{
printf("文件打开失败!\n");
perror("open");
_exit(-1);
}
k=fclose(file);
if(k==0)
{
printf("文件关闭成功!\n");
}
else
{
printf("文件关闭失败!\n");
perror("close");
_exit(-1);
}
}
我有个朋友是爵士音乐家,他有次跟我说:如果你不是乐队里最差的演奏者,马上换支乐队。我现在在Spotify工作,在这里我每天都觉得自己是个【狗屁程序员】,也因此我离开了上一家公司,从Spotify重头开始。当我在这里再也感觉不到自己是个【狗屁程序员】的时候,我会选择离开,然后找一个能让我再次感到自己是坨屎的地方。这种策略在我的职业生涯里确实 帮了我大忙。
------译自 Quora