Linux应用编程--IO文件

【正点原子】I.MX6U嵌入式Linux C应用编程指南V1.1.pdf (gitee.com)

Linux应用编程--IO文件_第1张图片

1.基本概述

1.什么是Linux应用程序

(1)整个嵌入式linux核心课程包括5个点,按照学习顺序依次是:裸机,c高级,uboot和系统移植,linux应用编程和网络编程,驱动

(2)典型的嵌入式产品就是基于嵌入式linux操作系统来工作。典型的嵌入式产品的研发是:

第一步让linux系统在硬件上跑起来(系统移植工作)

第二部是基于linux系统来开发应用程序实现产品功能。

(3)基于Linux去做应用编程,其实就是通过调用Linux的系统API来实现应用想要完成的任务。

Linux应用编程--IO文件_第2张图片

Linux应用编程--IO文件_第3张图片

2.应用程序和驱动程序,单片机程序

与驱动程序的不同:

应用程序是在用户空间,驱动程序在内核程序

与单片机的不同:

单片机程序是裸机,没有操作系统的概念,应用程序有操作系统的概念,并且运行在操作系统之上

单片机程序是硬件驱动+业务逻辑,应用程序是分离的

Linux应用编程--IO文件_第4张图片

3.如何编写Linux程序

Linux应用编程--IO文件_第5张图片

2.文件IO

2.1 什么是文件I/O

Linux应用编程--IO文件_第6张图片

2.2 文件操作的一般步骤

文件操作的主要接口API

(1)API是一些函数,这些函数是由linux系统提供支持的,由应用层程序来使用

(2)应用层程序通过调用API来调用操作系统中的各种功能,来干活

(2)学习一个操作系统,其实就是学习使用整个操作系统的API

linux常用文件IO接口

open close write read lseek

Linux应用编程--IO文件_第7张图片

2.3 文件读写完整过程

(1)强调:我们对文件进行操作的时候,一定要先打开文件,打开成功后才能去操作(如果打开本身失败,后就不用操作),最后读写完成之后一定要close关闭文件,否则可能会造成文件损坏

(2)文件平时是存在块设备中的文件系统中的,我们把这种文件叫做静态文件(平时未打开的文件)。当我们去open打开一个文件时,linux内核做的操作包括:

        1.1内核在进程中建立一个打开文件的数据结构,记录下我们打开的这个文件,

        1.2 内核在内存中申请一段内存,并且将静态文件的内容从块设备中读取到内存中特定地址管理存放(叫动态文件

(3)打开文件后,以后对这个文件的读写操作,都是针对内存中这一份动态文件的,而且不是针对静态文件的。当我们对动态文件进行读写后,此时内存中的动态文件和块设备中的静态文件就不同步了,当我们close关闭动态文件的时候,close内部内核将内存中的动态内容去更新(同步)块设备中的静态文件。

(4)常见的一些现象:

第一个:打开一个文件时比较慢(因为从块设置读取文件到内存中需要时间)

第二个:我们写的文件,如果没有进行保存而是直接关机/断电,重启后文件内容丢失。(因为内存和块设置未进行更新)

(5)为什么要这样设计?

因为块设备本身有读写限制(回忆NnadFlash,SD等块设备的读写特征),本身对块设备进行操作非常不灵活,而内存可以按字节未单位进行操作,而且可以随机操作(内存叫RAM,random),很灵活,所以内核设计文件操作时就这么设计。

2.4 文件描述符

(1)文件描述符其实实质是一个数字,这个数字在一个进程中表示一个特定的含有,当我们open打开一个文件时候,操作系统在内存中构建一些数据结构来表示这个动态文件,然后返回给应用程序一个数字作为文件描述符,这个数字就和我们内存中维护这个动态文件的这些数据结构挂钩进行区分。

(2)一句话讲清楚文件描述符:文件描述符就是用来区分一个程序打开的多个文件

(3)文件描述符的作用域是当前进程,出了当前进程这个文件描述符就没有意义。

Linux应用编程--IO文件_第8张图片

分配一个没有被使用的最小非负整数

Linux应用编程--IO文件_第9张图片

2.5 一个简单的文件读写实例

1.打开文件与关闭文件

(1)linux中的文件描述符fd的合法范围是0或者正正数,不可能是负数。

(2)open返回的fd程序必须记录好,以后向这个文件的所有操作都要靠这个fd去对应这个文件,最后关闭文件时也需要fd去指定关闭这个文件。如果我们在关闭文件fd丢掉了,这个文件就无法打开或者读写了。

#include
#include
#include
#include
#incldue

int main(int argc,char *argv[]){
	int fd=-1;//fd:file descriptor,文件描述符
	//第一步:打开文件
	fd=open("a.txt",O_RDWR);
	if(-1==fd){//也可以是fd<0
		printf("文件打开错误");
	}else{
		printf("文件打开成功,fd=%d.\n",fd);
	}
	//第二步:读写文件
	
	//第三步:关闭文件
	close(fd);
	return 0;
}

2.实时查询man手册

(1)当我们写应用程序时候,很多API不记得

(2)man 1 xx 查看linux shell命令

        man 2 xx 查看API

        man 3 xxx 查库函数

3. 读取文件内容

(1)ssize_t read (int fd,void *buf,size_t count);

        fd:表示要读取哪一个文件,fd一般由前面的open得到

        buf:是应用程序自己提供的一段内存缓存区,用来存储读出的内容

        count:是我们要读取的字节数

返回值ssize_t类型是linux内核用typedef重定义的一个类型(其实就是int),返回值表示成功读取的字节数。

#include
#include
#include
#include
#incldue

int main(int argc,char *argv[]){
	//定义一个缓冲区,这里要初始化,要不然都是随机的
	char buf[100]={0};
	//读取的返回值
	//这里设置为-1,是为下面判断是否读取成功,如果还是小于0,则读取失败
	int ret=-1;
	
	//第二步:读写文件
	ret= read(fd,buf,20);
	if(ret<0){
		printf("读取失败");
	}else{
		printf("读取成功,实际读取了%d字节.\n",ret);
		printf("文件内容是:[%s]\n",buf);
	}
	
	//第三步:关闭文件
	close(fd);
	return 0;
}

4.向文件中写入

(1)写入用write系统调用,write的原型和理解方法和read相似

(2)注意const在buf前面的作用,结合c语言高级专题中的输入型参数和输出型参数一节来理解。

(3)注意buf的指针类型是void(因为系统不知道用户要输入的buf的类型是什么)

(4)刚才先写入12字节,然后读出结果是0,(但是成功了)!!!!!!!!--->文件指针【因为我们write12字节的时候,文件指针指向第12个字节,而且读写指针是公用的,所以read的时候,指针指向第12字节】

#include
#include
#include
#include
#incldue
#include

int main(int argc,char *argv[]){
	int fd=-1;//fd:file descriptor,文件描述符
	//定义一个缓冲区,这里要初始化,要不然都是随机的
	char buf[100]={0};
	char writebuf[20]="I love linux";
	//读取的返回值
	//这里设置为-1,是为下面判断是否读取成功,如果还是小于0,则读取失败
	int ret=-1;
	
	//第一步:打开文件
	fd=open("a.txt",O_RDWR);
	if(-1==fd){//也可以是fd<0
		printf("文件打开错误");
	}else{
		printf("文件打开成功,fd=%d.\n",fd);
	}
	//第二步:读写文件
	//读文件
	ret= read(fd,buf,20);
	if(ret<0){
		printf("读取失败");
	}else{
		printf("读取成功,实际读取了%d字节.\n",ret);
		printf("文件内容是:[%s]\n",buf);
	}
	
	//写文件
	ret=write(fd,writebuf,strlen(writebuf));
	if(ret<0){
		printf("写入失败");
	}else{
		printf("写入成功,写入了%d\n",ret);
		printf("文件内容是:[%s].\n",buf);
	}
	
	//第三步:关闭文件
	close(fd);
	return 0;
}

2.6 打开文件:open()函数

Linux应用编程--IO文件_第10张图片

flags:标志

Linux应用编程--IO文件_第11张图片

Linux应用编程--IO文件_第12张图片

O_CREAT:当没有文件,会自动创建

测试一个文件是否存在:O_CREATE,O_EXCL

在Linux中查看

man 2 open

pathname:文件路径:

字符串类型,用于标识需要打开或创建的文件,可以包含路径(绝对路径或相对路径)信 息,譬如:"./src_file"(当前目录下的 src_file 文件)、"/home/dengtao/hello.c"等;如果 pathname 是一个符号 链接,会对其进行解引用。

mode:权限

Linux应用编程--IO文件_第13张图片

Linux应用编程--IO文件_第14张图片

Linux应用编程--IO文件_第15张图片

返回值:

2.7 open函数的flag详解

1. 读写权限:O_RDONLY(只读) O_WRONLY【只写】 O_RDWR【可读可写】

(1)linuc中文件有读写权限,我们在open打开文件时候也可以附带一定的权限说明(譬如O_RDONLY就表示以只读方式打开,O_WRONLY表示以只写方式打开,O_RDWR表示可以读可以写方式打开

(2)当我们附带了权限后,打开的文件就只能按照这种权限来操作

2.打开存在并有内容的文件时:O_APPEND【添加】,O_TRUNC【替换】

fd=open("a.txt",O_RDWR  | O_ADDEND | O_TRUNC)

(1)思考一个问题:当我们打开一个已经存在并且内部有内容的文件时会怎么样?

可能结果1:新内容会替代原来的内容(原来的内容以及不见了)

可能结果2:新内容添加在前面,原来的内容继续在后面

可能结果3:新内容附加在后面,原来的内容还在前面

可能结果4:不读不写的时候,原来的内容还在前面

(2)O_TRUNC属性去打开文件时,如果这个文件中本来是有内容的,则原来的内容会被丢弃。【结果1】

(3)O_ADDEND属性去打开文件时,如果中国文件中本来是有内容的,则新写入的内容会接续到原来内容的后面【结果3】

(4)默认不使用O_ADDEND和O_TRUNC【属性时就是结果4】

(5)如果O_APPEND和O_TRUNC同时出现会怎么样???O_TRUNC起作用。

3.exit,_exit,_Exit退出进程

exit(-1)
_eixt(-1)
_Exit(-1)

(1)当我们程序在前面步骤操作失败导致后面的操作没有可能运行下去的时候,应该在前面的错误监测中结束整个程序,不应该继续让程序运行下去。

(2)我们如何退出程序?

第一种:在mian用return,一般原则是程序正常终止return 0 ,如果程序异常终止则return -1

第二种:正式终止进程(程序)应该使用exit或者_exit或者_Exit之一。

4.打开不存在的文件时:O_CREAT(创建),O_EXCL(提示)

fd=open("a.txt",O_RDWR | O_CREAT | O_EXCl ,0666)

(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_CREAT标志去创建文件的时候,可以使用第三个参数mode来指定要创建的文件的权限。mode使用4个数字来指定权限的,其中后面三给很重要,对应我们要创建的这个文件的权限标志。(比如:我们要创建一个可读可写不可以执行的文件就用0666)

5. O_NONBLICK【阻塞与非阻塞】

(1)阻塞和非阻塞。

如果一个函数是阻塞式的,则我们调用这个函数当前进程有可能被卡住(阻塞式,实质是这个函数内部要完成的时候条件不具备,当前没办法做,要么等条件成熟),函数被阻塞住就不能立即返回;如果一个函数是非阻塞式的,那么我们调用这个函数后一定会立即返回,但是函数有没有完成任务不一定。

阻塞和非阻塞是两种不同的设计思路,并没有好坏。总的来说,阻塞式的结果有保障但是没有时间保障;非阻塞式的时间有保障但是结果没有保障

操作系统提供的API和由API封装而成的库函数,有很多本身就是被设计为阻塞式或者非阻塞式的。

我们打开一个文件默认是阻塞式,如果希望以非阻塞式的方式打开,则flag中要加上O_NONBLOCK标志

(2)只用于设备文件,而不能用于普通文件

6.O_SYNC

(1)write阻塞等待底层完成写入才返回到应用层
(2)无O_SYNC时write只是将内容写入顶层缓存区即可返回。

无O_SYNC时write只是将内容写道底层缓冲区即可返回,然后底层(操作系统中赋值实现open,write这些操作的哪些代码,也包含os中读写硬盘等底层硬件的代码)在合适的时候将buf中的内容一次性同步到硬盘。这种设计是为了提升硬盘操作的性能和销量,提升硬件寿命,但是有时候我们希望硬件不好等待,直接将我们的内容写入到硬盘中,这个时候就可以用O_SYNC标志。

2.8 写文件:write()函数

Linux应用编程--IO文件_第16张图片

Linux应用编程--IO文件_第17张图片

位置偏移量

Linux应用编程--IO文件_第18张图片

2.9 读文件:read()函数

Linux应用编程--IO文件_第19张图片

Linux应用编程--IO文件_第20张图片

Linux应用编程--IO文件_第21张图片

2.10 关闭文件:close()函数

open函数调用结束的时候,会返回一个非负整数的一个文件描述符,这个非负整数会与这个文件进行绑定。当close关闭的时候,这个非负整数会与这个文件进行解绑。

Linux应用编程--IO文件_第22张图片

如果没有手动设置close函数,则文件会在退出程序的时候,自动进行close

2.11 一些细节

1.errno和perror

(1)erron就是error number,意思是错误号码。linux系统中对各种常见错误做了一个编码,当函数执行错误的时候,函数会返回一个特定的errno编号告诉我们这个函数哪里出错。

(2)erron是由OS来维护的一个全局变量,任何os奴婢函数都可以通过设置errno来概述上层调用者究竟刚才发生了一个什么错误。

(3)erron本身实质是一个int类型的数字,每一个数字编号对应一种错误。当我们只看errno时只能得到错误编码。

(4)linux系统提供了一个函数perror(意思是print error),perror函数内部会读取出errno并且将这个不好认识的数字直接给转换成对应的错误信息字符串,然后printf打印出来。

2.read和write的count

(1)count和返回值的关系。count参数表示我们想要写或者读的字节数,返回值表示实际完成的要写或者要读的字节数。实现的有可能等于想要读写,也有可能小于(说明没有完成任务)

(2)count和阻塞与非阻塞结合起来,就会更加赋值。如果一个函数是阻塞式的,则我们要读取30个,结果暂时只有20个时就会被阻塞珠,等待剩余10个可以读。

(3)有时候我们写正式程序的时候,我们要读取或者写入的是一个很大的文件(例如文件有2MB,)我们不能把count设置为2*1024*1024,而应该把count设置为一个合适的数字(例如2048,4096),然后通过多次读取来实现全部读完。

3.文件IO效率和标准IO

Linux应用编程--IO文件_第23张图片

3.Linux系统如何管理文件

3.1 硬盘中的静态文件和inode(i节点:静态文件)

inode:硬盘用来记录文件信息的数据结构

(1)文件平时都存放在硬盘中的,硬盘中存储的文件是以一种固定的形式存放的,我们叫静态文件

(2)一块硬盘中可以分为两大区域:一个是硬盘内容管理表项,另一个是真正存储内容的区域。操作系统访问硬盘是先去读取硬盘内容管理表,从中找到我们要访问的那个文件的扇区级别的信息。如何在用这个信息区查询真正存储内容的区域,最后得到我们要的文件。

(3)操作系统最初拿到的信息是文件名,最终得到的是文件内容。第一步就是区查询硬盘内容管理表,这个管理表中以文件为单位记录了各个文件的各种现象,每一个文件有一个信息列表(我们叫inode,i节点,其实实质是一个结构体,这个结构体有很多元素,每一个元素记录这个文件的一些信息,其中就包括文件名,文件在硬盘上对应的扇区号,块号

(4)强调:硬盘管理的时候是以文件为单位的,每一个文件一个inode,每一个inode有一个数字编号,对应一个结构体,结构体中记录了各种信息。

(5)联系平时实践,格式化硬盘的时候,发现有:快速格式化和底层格式化。

快速格式化非常快,格式化一个32G的U盘只要1秒,

普通格式化速度慢,

差异???

其实快速格式化就是只是删除了U盘中的硬盘内容管理表(其实就是inode,真正存储的内容没有被删除。这种格式化的内容是有可能被找回)

3.2 内存中被打开的文件和vnode(v节点:动态文件)

(1)一个程序的运行就是一个进程,我们在程序中打开的文件就属于某一个进程。每一个进程都有一个数据结构,用来记录这个进程的所有信息(叫进程信息表),其中表中有一个指针会指向一个文件管理表,文件管理表中记录了当前进程打开的所有文件机器相关信息。文件关联表中用来索引各个打开的文件的index就是文件描述符fd,我们最终找到的就是一个已经被打开的文件管理结构体vnode

(2)一个vnode中就记录一个被打开的文件的各种信息,而且我们只要知道这个文件的fd,就可以很容易的找到这个文件的vnode进而对这个文件进行各种操作。

3.3 文件与流的概念

流(stream)

(1)文件操作中,文件类似于是一个大包裹,里面装了一堆字符,但是文件被读/写入只能一个字符一个字符的进行,而不能一股脑的读写。那么一个文件中N多的字符被挨个一次读写时,这些字符就构成一个字符流。

(2)流这个概念是动态的,不是静态的。

(3)编程中提到流这个概念,一般都是IO相关,所以经常叫IO流。文件操作时就构成一个IO流。

4.lseek

1. lseek函数介绍

(1)文件指针:当我们要对一个文件进行读写时,一定要先打开这个文件,所以我们读写的所有文件都是动态的。动态文件在内存中的形态就是文件流的形式。

(2)文件流很长,里面有很多个字节。那么我们当前正在操作的是哪一个位置?GUI模式下的软件用光标来标识当前正在操作的位置。

(3)在动态文件中,我们会通过文件指针来标识这个正在操作的位置。所谓的文件指针,就是我们文件管理表这个结构体里面的一个指针。所以文件指针其实就是vnode中的一个元素。这个指针标识当前我们正在操作文件流的哪一个位置。这个指针不能被直接访问,linux系统通过lseek函数来访问这个文件指针。

(4)当我们打开一个空文件时,默认情况下文件指针指向文件流的开始,所以这时候区write时写入就是从文件开头开始的。write和read函数本身自带移动文件指针的功能,所以当我们write了n个字后,文件指针会自动向后移动n位。如果需要人为的随意更改文件指针,那只能通过lseek函数

(5)read和write函数都是从当前文件指针出开始操作的,所以我们用lseek显示的将文件指针移动号,那么再去read/write时就是从移动过后的位置开始。

(6)我们之前从空文件,先write写了12个字节,然后read的时候是空的(但是此时我们打开文件后发现12字节确实写进来------>是因为write了12字节,此时文件指针在第12个字节的位置,所以我们进行read操作的时候,文件指针指向12字节,所以为空)

2.lseek计算文件长度

(1)linux中并没有一个函数可以直接返回一个文件的长度。但是我们做项目时经常会需要知道一个问文件的长度。

int cal_len(const char *pathname){
    int fd=-1;
    int ret=-1;

    fd=open(pathname,O_RDONLY);
    if(-1==fd){
        perror("文件打开错误");
        return -1;
    }
    ret=lseek(fd,0,SEEK_END);
    return ret;
}
int main(int argc,char* args[]){
    int fd=-1;
    int ret=-1;
    if(argc!=2){
        printf("usage: %s filename",argv[0]);
    }
    printf("文件长度为:%d字节",cal_len(argv[1]));
    return 0
}

3.lseek构建空洞文件

(1)空洞文件就是文件中有一段是空的

(2)普通文件中间是不能是空的,因为我们write时文件指针是依次从前到后去遍历的,不可能绕过前面直接到后面

(3)我们打开一个文件后,用lseek往后跳过一段,再write写入一段,就活构成空洞文件

(4)空洞文件方法对多线程共同操作文件是及其有用的。有时候我们创建一个很大的文件,如果从头开始依次构建时间很长。有一种思路就是将文件分为多段,然后多线程来操作每一个线程负责其中一段的写入。

5. 文件共享【O_ADDEND的使用】

1.重复打开同一个文件读取:使用不同的文件指针

普通文件允许被重复打开

(1)一个进程中两次打开同一个文件,然后分别读取,看结果如何?

(2)结果:fd1和fd2分别读--->abcdabcdabcd

(3)分别读说明:我们使用open两次打开同一个文件时,fd1和fd2所对应的文件指针是不同的2个独立的指针。文件指针是包含再动态文件的文件管理表中,所以可以看出linux系统的进程中不同fd对应的是fd对应的是不同的独立的文件管理表。

2.重复打开同一个文件写入

(1)一个进程中2个打开同一个文件,得到fd1和fd2,然后看看是分别写,还是一起写。

        fd1 ab   fd2 cd

        分别写:cdcdcdcd【因为使用同一个文件指针,所以后会将前面的覆盖掉】

        单独写:abcdabcd

(2)正常情况下我们有时候需要分别写,有时候又需要接续写,所以这两个本身是没有好坏之分。关键看用户需求。

3.加O_APPEND解决同时读写覆盖问题

fd=open("a.txt",O_RDWR | O_TRUNC | O_CREAT | O_APPEND ,0666);
O_RDWR :表示可读可写
O_TRUNC 和 O_CREAT :一起使用,表示可以遇到要打开不存在的文件,这个时候可以自己自动创建
O_APPEND :是当出现一个文件被同时打开多次,可以使得读写是接着的。

(1)一时候我们希望接续写而不是分别写,办法就是再open时加O_APPEND标志即可

4.O_APPEND的实现原理和其原子操作性说明

(1)O_APPAND为什么可以将分别写改为接续写?

        关键点是文件指针。分别写的内部原理是2个fd拥有不同的文件指针,并且彼此只考虑自己的位移。但是O_APPEND标志可以让write和read函数内部多做一件事情,就是移动自己的文件指针的同时也去把别人的文件指针同时移动。(也就是是即使加了O_ADDEND,fd1和fd2还是拥有一个独立的文件指针,但是这两个文件指针关联起来,一个动了会通知另一个跟着动)

(2)O_APPAND对文件指针的影响,对文件的读写是原子的

(3)原子操作的含意:整个操作一旦开始是不会被打断的,必须知道操作结束其他代码才能得以调度运行,这就叫原子操作。每一种操作系统中都有一些机制来实现原子操作,以保障哪些需要原子操作的任务可以运行。

6.文件共享的实现方式

1.什么是文件共享

(1)文件共享就是同一个文件【同一个文件指的是同一个inode,同一个pathname】被多个独立的读写体(多个文件描述符)同时操作(一个打开尚未关闭的同时另一个去操作)。

(2)文件共享的意义有很大:比如,可以通过文件共享来实现多线程同时操作同一个大文件,以减少文件读写时间,提升效率。

2.文件共享的3种实现方式

Linux应用编程--IO文件_第24张图片

文件指针如果是关联【使用同一个文件指针】的,则表示接续写-->abababab

文件指针如果是不关联【文件指针是分开的,一个进程对应一个指针】的,表示分开写--->cdcdcd

(1)文件共享的核心 :就是怎么弄出来多个文件描述符指向同一个文件。

方式一:同一个进程中多次使用open打开同一个文件

Linux应用编程--IO文件_第25张图片

方式二:再不同进程中去使用open打开同一个文件

Linux应用编程--IO文件_第26张图片

这时候因为两个fd再不同进程中,所以两个fd的数字可以相同也可以不相同【因为即使两个文件描述符一致,但是进程不同,所其效果互相不影响】

方式三:linux系统提供了dup和dup2两个API来让进程复制文件描述符

Linux应用编程--IO文件_第27张图片

(2)我们分析文件共享时的核心关注点在于:分别读写还是/接续读写。

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.也就是标准输入,标准输出,标准错误。

(7)标准输入一般对应的是键盘(可以理解未:0这个fd对应的是键盘的设备文件),标准输出一般是LCD显示器(可以理解为:1对应LCD的设备文件)

(8)printf函数其实就是默认输出到标准输出stdout上。stdio中有一个函数叫fprintf,这个函数可以指定输出到哪一个文件描述符中。

7.文件描述符的复制--dup

1使用dup进行文件描述符复制

1、int dup(int oldfd)
作用:将oldfd文件描述符复制一份放置在最小未被使用的文件描述符中。
2、 int dup2(int oldfd, int newfd);
 作用:复制文件描述符,可以用参数newfd指定新的文件描述符值。

(1)dup系统调用对fd进行复制,会返回一个新的文件描述符(比如原理的fd是3,返回的就是4)

(2)dup系统调用有一个特点,就是自己不能指定复制后得到fd的数字是多少,而是由操作系统内部自动分配的,分配原则是遵守fd分配的原则。

(3)dup返回的fd和原来的oldfd都指向oldfd打开的那个动态文件,操作这两个fd实际操作的都是oldfd打开的那个文件。实际上构成文件共享。

(4)dup返回的fd和原来的oldfd同时向一个文件写入时,结果是分别写还是接续写?

2.使用dup的缺陷分析

(1)dup并不能指定分配的新的文件描述符的数字。dup2系统调用修复了这个缺陷,所以平时项目中实际使用时根据具体情况来决定使用dup还是dup2.

3.标准输出的重定位:

(1)之前讲到0,1,2讲到这三给标准输入,输出,错误通道占用。并且我们可以关闭这三个

(2)使用close(1)关闭标准输出,关闭后我们printf输出到标准输出的内容就看不到了

(3)然后我们可以使用dup重新分配得到1这个fd,这时候就把oldfd打开这个文件和我们1这个标准输出通道给绑定起来。这就叫标准输出的重定位。

(4)使用close和dup配合进行文件的重定向。

#define FILENAME "1.txt"
int mian(){
    int fd1=-1;
    int fd2=-1;
    fd1=open(FILENAME,O_RDWR | O_CREAT | O_TRUNC,0644);
    if(fd<0){
        perror("open");
        return -1;
    }
    printf("fd1=%d\n,fd1);
    close(1);//1就是标准输出stdout,此时表示将1进行释放
    //复制文件描述符
    //fd2一定等于1,因为前面刚刚关闭了1,这句话就把1.txt文件和标准输出就绑定起来,所以以后输出到标准输出的信息就可以到1.txt中查看
    fd2=dup(fd1);
    printf("fd2=%d",fd2);

    close(fd1);
    return 0;
}

4. 使用dup2进行文件描述符复制

fd2=dup2(fd1,16)

(1)dup2和dup的作用一样的,都是复制一个新的文件描述符。但是dup2允许用户指定新的文件描述符的数字。

(2)使用方法看man手册原型即可。

#define FILENAME "1.txt"
int mian(){
    int fd1=-1;
    int fd2=-1;
    fd1=open(FILENAME,O_RDWR | O_CREAT | O_TRUNC,0644);
    if(fd<0){
        perror("open");
        return -1;
    }
    printf("fd1=%d\n,fd1);
    fd2=dup2(fd1,16);
    printf("fd2=%d",fd2);

    close(fd1);
    return 0;
}

5.dup2共享文件交叉写入测试

(1)dup2复制的文件描述符,和原来的文件描述符虽然数字不一样,但是这个连个指向同一个打开的文件

(2)交叉写入的时候,结果是接续写(abcdabcd)

6.命令行中重定向命令:>

ls >  1.txt:表示将ls的结果写入1.txt

(1)linux中的shell命令执行后,打印结果都是默认进入stdout(本质上是因为这些命令比如ls,pwd等都是调用printf进行打印的),所以我们可以再linux的终端shell中直接看到命令执行的结果。

(2)能否想办法把ls,pwd等命令的输出重定位到一个文件(2.txt)去,实际上linux终端支持一个重定位的符号>很简单可以做这些点

(3)这个>的实现原理,实际就是利用open+close+dup,open打开一个文件2.txt,然后close关闭stdout,然后dup将1和2.txt文件关联起来

8.fcntl函数

1.fcntl的原型和作用

(1)fcntl函数是一个多功能文件管理的工具箱,接收2个参数+1个变参。第一个参数是fd表示要操作哪一个文件,第二个参数是cmd表示要进行哪一个命令操作。变参是用来传递参数的,要配合cmd使用。

(2)cmd的样子类似于F_XXX,不同的cmd具有不同的功能。学习的时候没办法把所有的cmd含义弄清楚。

2.fcnt1的常用cmd

man fcntl

1. F_DUPFD:类似于dup

(1)这个cmd的作用是复制文件描述符(作用类似于dup和dup2),这个命令的功能是从可用的fd数字列表中找到一个比arg(第三个参数)大或者和arg一样大的数字作为oldfd的一个复制的fd,和dup2有点像但是不同。dup2返回的就是我们指定的那个newfd否则就会出错,但是F_DUPFD命令返回的是>=arg的最小的哪一个数字。

Linux应用编程--IO文件_第28张图片

3.使用fcntl模拟dup2

int main(){
    int fd1=-1,fd2=-1;
    fd1=open("1.txt:,O_RDWR | O_CREAT | O_TRUNC ,0644);
    if(fd1<0){
        perror("open"); 
        return -1;
   }
    printf("fd1=%d",fd1);

    //fcntl(fd1,F_DUPFD,6);//输出大于等于6

    close(1);//把1关闭了
    fcntl(fd1,F_DUPFD,0)://输出1

    printf("fd2=%d",fd2);
    while(1){
        write(fd1,"aa",2);
        sleep(1);
        write(fd2,"bb",2);
    }
    close(fd1);
    return -1;
}

9.标准IO库的介绍

1. 标准IO和文件IO有什么区别

(1)看起来使用都是函数,但是:标准IO是c库函数,而文件IO是linux系统的API【操作系统管理的】

(2)c语言库函数是由API封装而来的。库函数内部也是通过调用API来完成操作的,但是库函数因为多了一层封装,所以API要更加好用一些。

(3)库函数比API还有一个优势是:API再不同的操作系统之间是不能通用的,但是c库函数再不同操作系统中几乎一样的。所以c库函数具有可移植性而API不具有可移植性。

(4)性能上和易用性上看,c库函数一般要好要好一点。比如,IO,文件IO是不带缓存的,而标准IO是带缓存的,因此标准IO比文件IO性能更高。

2.常用标准IO函数介绍

(1)常见的标准IO库函数:fopen,fclose,fwrite,fread,ffulsh,fseek

3.一个简单的标准IO读写文件实例

int main(){
    FILE* fp=NULL;
    size_t len=-1;
    int array[10]={1,2,3,5,6};
    fp=fopen(FILENAME,"w+");
    if(NULL==fp)[
        perror("fopen");
        exit(-1);
    }
    printf("fopen success fp=%p",fp);
    
    //写读写文件
    //表示将“abcde”写入fp中,一个字节1bit,一共5个
    //len=fwrite("abcde",1,5,fp);
    len=fwrite(array,sizeof(int),sizeof(array)/sizeof(array[0]),fp);
    len=fwrite(array,4,10,fp);//实际上输出是10,而不是4*10
    fclose(fp);
    return 0;
}

Linux应用编程--IO文件_第29张图片

你可能感兴趣的:(linux,运维,服务器)