zoukankan      html  css  js  c++  java
  • Linux系统编程、网络编程-文件I/O

    第一章:文件io

    1. 文件io讲些什么

    文件io这一章讲的是,如何调用Linux OS所提供的相关的OS API,实现文件的读写。


    1.1 如何理解“文件IO”这个词

    IO就是input output的意思,文件io就是文件输入输出,也就是文件读写。

    读写的是什么?
    答:是数据。

    不过读文件和写文件,到底哪一个是input,哪一个是output呢?
    答:input和output,其实是以CPU作为参考点来看的:

    o(写)
    C ——————————> 文
    P <—————————— 件
    U i(读)



    疑问:为什么不能越过OS,直接操作文件呢?

    答:当有OS的时候,应用程序基于OS运行时,必须通过OS API假借OS之手,才能操作底层硬件,无法回避。



    1.2 本章所涉及的OS API(系统函数)有哪些

    (1)open函数:打开文件

    (2)close函数:关闭文件

    (3)read函数:从打开的文件读数据

    (4)write函数:向打开的文件写数据

    (5)lseek函数:移动在文件中要读写的位置

    (7)dup函数:文件读写位置重定位函数,本来是写到这个文件,重定位后可以写到另一个文件里面

    (11)fcntl函数:文件描述符设置函数

    (12)ioctl函数:一个特殊的函数


    本章的函数,在后续课程中会频繁使用,所以这些函数一定要会用,但是不要去死记,函数参数很多,想记也记不住,涉
    及细节时,使用man查一查自然就知道了。



    2 文件读写的简单例子

    2.1 文件操作三步曲

    (1)第一步:打开文件
    open函数

    (2)第二步:读、写等操作文件
    read、write函数

    (3)第三步:关闭文件
    close函数


    使用man命令查看这些函数的详细说明。


    (4)程序演示

    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <unistd.h>
    
    
    int main(void)
    {
    	int fd = 0;
    
    	fd = open("./file.txt", O_RDWR);
    	if(-1 == fd)
    	{
    		printf("open fail
    ");
    		return 0;
    	}
    	else
    	{
    		printf("open ok
    ");
    	}
    
    	
    	char buf1[] = "hello world";
    	write(fd, (void *)buf1, 11);
    
    	lseek(fd, 0, SEEK_SET);
    
    	char buf2[30] = {0};
    	read(fd, buf2, sizeof(buf2));
    	
    
    	printf("buf2 = %s
    ", buf2);	
    	
    	
    	close(fd);
    	
    	return 0;
    }
    

      




    程序解释
    1)open函数

    (a)利用文件名,通过文件系统找到块设备上的文件
    图:

    · 文件系统就是一个程序代码,组织管理着块设备上的所有文件
    · 文件系统属于OS的一部分


    (b)找到文件后,调用块设备驱动程序,打开文件
    驱动程序也是属于OS的一部分。

    · 打开成功:返回一个非负整数的操作符(文件描述符)
    · 打开失败:返回-1,表示打开失败

    使用fd存放返回值,便于我们使用这个返回值。
    文件打开成功后就可以操作文件了。


    2)write函数
    利用打开成功后返回的,非负整数的文件描述符,向文件里面写数据。


    3)lseek函数
    利用文件描述符,将文件读写的位置调整到文件头上。

    为什么要调到文件头上?
    write时,文件读写的位置到了末尾了,read读末尾的话,读到的内容就是空的,看不到read的效果。


    4)read函数
    从文件头上开始,读取指定长度的数据到buf中。

    printf打印显示,查看效果。


    2.2 open打开文件时,open具体做了哪些事情

    如果open打开失败的话(比如文件不存在就会导致失败),open啥也不会干就直接返回了。

    但是如果open将文件打开成功的话,open具体做了哪些事情呢?


    1)记录打开文件的信息

    (a)程序运行起来后就是一个进程了,OS会创建一个task_struct的结构体,记录进程运行时的各种信息,比如所打开文件的相关信息
    图:

    (b)open将文件成功打开后,在task_struct中又会创建一些结构体(数据结构),用于记录当前进程目前所开文件的信息,
    后续所有的文件操作,都需要依赖于这些信息,其中就包括指向打开文件的文件描述符。


    2)open函数会申请一段内存空间(内核缓存),后续读写文件时,用于临时缓存读写文件时的数据
    图:


    (a)什么是缓存?
    就是开辟的一段内存空间,比如char buf[100],这就是一段100字节的缓存空间,用于临时存放中转的数据。



    (b)为什么叫内核缓存?
    open是OS所提供的系统函数,属于OS内核的一部分,所以open函数所开辟的缓存空间,就是内核缓存。


    在我们的例子程序中,我定义了buf1和buf2这两个数组,这两个数组是我自己的应用程序定义的,因此就叫应用缓存。
    图:


    其实定义的一切变量空间,比如int a,int b[30]等,都是缓存,都是用于临时缓存数据,只是我们以前没有这么叫过。


    (d)open为什么要开内核缓存空间?

    内存读写速度 > 磁盘读写速度的,有了在内存中开辟的内核缓存后,上层读写数据时,会直接读写缓存,速度会很快,至于缓存
    与磁盘上文件数据的交换,就留给下层去做,这样可以节省上层操作的时间。


    (e)注意:open时只是开辟了内核缓存空间,里面并没有数据,只有当进行读写数据时,才会缓存读写的数据。


    (f)读写时,缓存间数据的流动是怎样的?
    图:




    2. open函数

    2.1 函数原型
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>

    int open(const char *pathname, int flags);
    功能:只能打开存在的文件,如果文件不存在就返回-1报错。

    int open(const char *pathname, int flags, mode_t mode);
    功能:如果文件存在就直接打开,如果文件不存在,就按照mode指定的文件权限,创建一个该名字的新文件。

    也就是说三个参数时,不仅包含打开已存在文件的功能,还多了一个创建文件的功能。


    2.1 open函数返回值
    如果打开成功,返回一个非负整数的文件描述符。

    如果打开失败,返回-1,并且设置错误号给系统定义的全局变量errno,用于标记函数到底出了什么错误。

    有关errno和错误号,后面会详细介绍。


    2.3 open函数的重点:flags参数
    参数1:pathname,表示路径名,很简单
    参数3:mode,创建文件时,用于指定文件的原始权限,其实就是rwxrwxr--。



    2.3.1、flags 之 O_RDONLY、O_WRONLY、O_RDWR、O_TRUNC、O_APPEND

    · flags的作用
    flags用于指定文件的打开方式,这些宏还可以使用|组合,比如O_RDONLY | O_APPEND,同时指定多个宏。
    不要被这些宏吓到了,理解了,用多了,自然就熟悉了,不用可以去记住,想不起来了,使用man查看open函数就能知道。

    这些宏对应的就是一些整形数,#define O_RDONLY 2。

    · 这些宏被定义在了那里?
    定义在了open所需要的头文件中,使用open函数时,必须要包含对应的头文件,否者,这些宏你就用不了。


    (1)宏的含义
    (a)O_RDONLY:只读方式打开文件,只能对文件进行读

    (b)O_WRONLY:只写方式打开文件,只能对文件记性写

    (c)O_RDWR:可读可写方式打开文件,既能读文件,也能写文件

    以上这三个在指定时,只能唯一指定,不可以组合,比如O_RDONLY|O_WRONLY。


    (d)O_TRUNC:打开时将文件内容全部清零空

    (e)O_APPEND:打开文件后,写数据时,原有数据保留,新写的数据追加到文件末尾,此选项很重要。
    APPEND本来就是追加的意思。

    如果不指定这个选项的话,新写入的数据会从文件头上开始写,覆盖原有的数据,所有以后会经常使用这个选项。

    (f)O_NONBLOCK和O_ASYNC:后面的课程用到后,再来介绍。



    (2)代码演示

    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <unistd.h>
    
    
    int main(void)
    {
    	int fd = 0;
    	int ret = 0;
    
    	fd = open("./file.txt", O_RDWR);
    	//fd = open("./file.txt", O_RDWR|O_APPEND);
    	//fd = open("./file.txt", O_RDWR|O_TRUNC);
    	//fd = open("./file.txt", O_RDONLY);
    	//fd = open("./file.txt", O_WRONLY);
    	if(-1 == fd)
    	{
    		printf("open fail
    ");
    		return 0;
    	}
    	else
    	{
    		printf("open ok
    ");
    	}
    
    	
    	char buf1[] = "hello world";
    	ret = write(fd, (void *)buf1, 11);
    	if(ret == -1)
    	{
    		printf("write fail
    ");
    	}
    
    
    	lseek(fd, 0, SEEK_SET);
    
    	char buf2[30] = {0};
    	ret = read(fd, buf2, sizeof(buf2));
    	if(-1 == ret)
    	{
    		printf("read fail
    ");
    	}
    
    	printf("buf2 = %s
    ", buf2);	
    	
    	
    	close(fd);
    	
    	return 0;
    }
    

      

    2.3.2 flags参数 之 O_CREAT、O_EXCL。

    (1)O_CREAT

    1)open两个参数时的缺点
    只能用于打开已经存在的文件,如果文件不存在就返回-1报错。


    2)O_CREAT的作用

    可以解决两个参数的缺点,指定O_CREAT时,如果:

    (a)文件存在:直接打开
    (b)文件不存在:创建该“名字”的文件。


    不过指定O_CREAT,需要给open指定第三个参数mode,用于指定新创建文件的原始权限。
    int open(const char *pathname, int flags, mode_t mode);

    这个权限就是文件属性中的rwxrwxr--。


    (2)O_EXCL
    当O_EXCL与O_CREAT同时被指定,打开文件时,如果文件之前就存在的话,就报错。

    代码演示:

    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <errno.h>
    
    
    int main(void)
    {
    	int fd = 0;
    
    
    	//fd = open("./file1.txt", O_RDWR|O_CREAT|O_EXCL, 0664);
    	fd = open("./file.txt", O_RDWR|O_CREAT, 0664);
    	if(-1 == fd)
    	{
    		printf("open fail: %d
    ", errno);
    		return 0;
    	}
    	else
    	{
    		printf("open ok
    ");
    		printf("fd = %d
    ", fd);
    	}
    
    	
    	char buf1[] = "hello world";
    	write(fd, (void *)buf1, 11);
    
    	lseek(fd, 0, SEEK_SET);
    
    	char buf2[30] = {0};
    	read(fd, buf2, sizeof(buf2));
    	
    
    	printf("buf2 = %s
    ", buf2);	
    	close(fd);
    
    
    	
    	//fd = open("./file1.txt", O_RDWR|O_CREAT, 0664);
    	//printf("~~ fd = %d
    ", fd);
    	
    	
    	
    	return 0;
    }
    

      


    意义:保证每次open的是一个新的文件,如果文件以前就存在,提醒你open的不是一个新文件。

    后面具体用到了,你自然就知道O_EXCL的实用价值了。



    2.4 详论文件描述符

    2.4.1 什么是文件描述符

    open成功就会返回一个非负整数(0、1、2、3...)的文件描述符,比如我们示例程序中open返回的文件描述符是3。

    文件描述符指向了打开的文件,后续的read/write/close等函数的文件操作,都是通过文件描述符来实现的。



    2.4.2 文件描述符池

    每个程序运行起来后,就是一个进程,系统会给每个进程分配0~1023的文件描述符范围,也就是说每个进程打开文件时,open所
    返回的文件描述符,是在0~1023范围中的某个数字。

    0~1023这个范围,其实就是文件描述符池。

    1023这个上限可不可以改?
    可以,但是没有必要,也不会介绍如何去改,因为一个进程基本不可能出现,同时打开1023个文件的情况,文件描述符的数量
    百分百够用。



    2.4.3 在我们的例子中,为什么open返回的是3

    open返回文件描述符是由规则的:

    规则就是,open返回文件描述符池中,当前最小没用的哪一个。


    进程一运行起来,0/1/2默认就被使用了,最小没被用的是3,所以返回3。

    如果又打开一个文件,最小没被用的就应该是4,所以open返回的应该是4。
    演示:



    疑问:0、1、2被用来干嘛了,后面解释。


    2.4.4 文件关闭后,被文件用的描述符怎么办
    会被释放,等着下一次open时,被重复利用。

    演示:



    2.4.5 open的文件描述符 与 fopen的文件指针

    (1)open:Linux 的系统函数(文件io函数)
    open成功后,返回的文件描述符,指向了打开的文件。


    (2)fopen:C库的标准io函数

    #include <stdio.h>
    FILE *fopen(const char *path, const char *mode);

    fopen成功后,返回的是FILE *的文件指针,指向了打开的文件。


    (3)对于Linux的C库来说,fopen 这个C库函数,最终其实还是open函数来打开文件的

    fopen只是对open这个函数做了二次封装。


    应用程序
    |
    |
    FILE* fopen标准io函数(c库函数)
    |
    |
    int open文件io函数(Linux系统函数)


    也就是说,fopen的文件指针,最终还是会被换成open的文件描述符,然后用于去操作打开的文件。
    讲第3章-C库的标准io函数时,会详细介绍文件指针与文件描述符之间的关系。


    2.5 errno和错误号

    在我们的例子中,如果open失败了,只是笼统的打印出“打开文件失败了”,但是并没有提示具体出错的原因,没有详细的出错
    原因提示,遇到比较难排查的错误原因时,很难排查出具体的函数错误。

    open失败,如何具体打印出详细的出错信息呢?
    这就不得不提errno的作用了。


    (1)什么是ernno?

    函数调用出错时,Linux系统使用错误编号(整形数)来标记具体出错的原因,每个函数有很多错误号,每个
    错误号代表了一种错误,产生这个错误时,会自动的将错误号赋值给errno这个全局变量。

    errno是Linux系统定义的全局变量,可以直接使用。

    错误号和errno全局变量被定义在了哪里?
    都被定义在了errno.h头文件,使用errno时需要包含这个头文件。

    man errno,就可以查到errno.h头文件。


    (2)打印出具体的出错原因

    1)printf打印出错误号
    演示:


    使用errno时,编译提示‘errno’ undeclare的错误,表示找不到errno全局变量。

    错误号确实标记了具体的出错原因,但是我们并不知道这个错误号,具体到底代表的是什么错误。
    就好像光给你某人的身份证号,仅凭这个身份证号你无法判断是那一个人,需要换成名字才成。


    2)perror函数

    perror函数可以自动将“错误号”换成对应的文字信息,并打印出来,方便我们理解。

    man perror
    perror是一个C库函数,不是一个系统函数。


    使用演示:



    perror的工作原理?
    调用perror函数时,它会自动去一张对照表,将errno中保存的错误号,换成具体的文字信息并打印出来,我们就
    能知道函数的具体错误原因了。



    3)man open,可以查看open函数,都有哪些错误号



    每个错误号代表了一种函数可能的出错情况,比如:

    EACCES:不允许你访问文件而出错。


    Liunx为了让错误号能够见名识意,都给这些整形的错误号定义了对应的宏名,这些宏定义都被定义在了error.h头文件中。

    man perror这个函数,也可以看到这个头文件。


    疑问:我是不是要必须记住这些错误号?

    答:反正我是记不住,记不住怎么办?

    根本不需要记住,使用perror函数,它可以自动翻译,我们讲错误号,只是希望你理解错误号这个东西,后面的课程
    会经常见到东西,到了后面我就不再介绍。


    3. close、write、read、0/1/2这三个文件描述符

    3.1 close函数

    3.1.1 功能
    关闭打开的文件。

    close(fd);

    就算不主动的调用close函数关闭打开的文件,进程结束时,也会自动关闭进程所打开的所有的文件。

    但是如果因为某种需求,你需要在进程结束之前关闭文件的话,就主动的调用close函数来实现。

    Linux c库的标准io函数fclose,向下调用时,调用就是close系统函数。



    3.1.2 close关闭文件时做了什么

    (1)open打开文件时,会在进程的task_struct结构中,创建相应的结构体,以存放打开文件的相关信息。


    (2)结构体的空间开辟于哪里

    open函数会通过调用类似malloc的函数,在内存中开辟相应的结构体空间,如果文件被关闭,存放该文件被打开时
    信息的结构体空间,就必须被释放,

    类似free(空间地址);

    不过malloc和free是给C应用程序调用的库函数,Linux系统内部开辟和释放空间时,用的是自己特有的函数。


    如果不释放,当前进程的内存空间,会被一堆的站着茅坑不拉屎的垃圾信息所占用,随后会导致进程崩溃,甚至是
    Linux系统的崩溃。

    这就好比一个挺大的仓库,每次废弃的物品都不及时清理,最后整个空间全被垃圾塞满,仓库还是那个仓库,但是仓库瘫
    痪了,无法被正常使用。

    因此close文件时,会做一件非常重要的事情,释放存放文件打开信息的结构体空间。


    (3)有关task_stuct结构体

    1)这个结构体用于存放进程在运行的过程中,所涉及到的各种信息,其中就包括进程所打开文件的相关信息。

    2)task_stuct结构体,什么时候开辟的?
    进程(程序)开始运行时,由Linux系统调用自己的系统函数,在内存中开辟的,

    代码定义个各种变量(int a、数组、结构体、对象等),开辟这些空间时,这些空间都是来自于内存。

    每一个进程都有一个自己的task_stuct结构体,用于记录自己的所有相关信息,这就好比每一个人,在公安局都有一份
    属于自己的档案信息,task_stuct结构体记录的就是,进程在活着时的一切档案信息。


    3)什么时候释放
    进程运行结束了,Linux系统会调用自己的系统函数,释放这个结构体空间,如果不释放的话,每运行一个进程就开辟
    一个,但是进程结束后都不释放,最后会导致Linux系统自己的内存空间不足,从而使得整个Linux系统崩溃。






    3.2 write

    3.2.1 函数原型
    #include <unistd.h>

    ssize_t write(int fd, const void *buf, size_t count);

    (1)功能:向fd所指向的文件写入数据。



    (2)参数
    1)fd:指向打开的文件
    2)buf:保存数据的缓存空间的起始地址
    3)count:从起地址开始算起,把缓存中count个字符,写入fd指向的文件


    数据中转的过程:
    应用缓存(buf)————>open打开文件时开辟的内核缓存——————>驱动程序的缓存——————>块设备上的文件

    (3)返回值
    调用成功:返回所写的字符个数
    调用失败:返回-1,并给errno自动设置错误号,使用perror这样的函数,就可以自动的将errno中的错误号翻译为文字。


    3.2.2 使用write

    (1)代码演示

    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <errno.h>
    
    
    int main(void)
    {
    	int fd = 0;
    
    
    	fd = open("./file.txt", O_RDWR|O_CREAT, 0664);
    	if(-1 == fd)
    	{
    		printf("open fail: %d
    ", errno);
    		return 0;
    	}
    	else
    	{
    		printf("open ok
    ");
    		printf("fd = %d
    ", fd);
    	}
    
    	
    	char buf1[] = "hello world";
    	//write(fd, (void *)buf1, 11);
    	
    	//write(fd, (void *)buf1+1, 10);
    	
    	//write(fd, "hello world", 9);
    	write(fd, "hello world"+1, 10);
    
    	
    	return 0;
    }
    

      



    (2)思考1:write(fd, buf+1, 10),写入时,是一个什么情况


    (3)思考2:write(fd, "hello world", strlen("hello world")),直接写字符串,可不可以?

    演示:


    (4)为什么直接写字符串可以?

    char buf[] = "hello world";
    write(fd, buf, 11));


    像这种情况,字符串直接缓存在了应用空间buf中,buf代表的数组第一个元素h所在空间的地址。


    直接写字符串常量时,字符串常量被保存(缓存)在了常量区,编译器在翻译如下这句话时,
    write(fd, "hello world", strlen("hello world"))


    会直接将"hello world"翻译为,"hello world"所存放空间的起始地址(也就是h所在字节的地址),换句话说,直接使
    用使用字符串常量时,字符串常量代表的其实是一个起始地址。


    strlen("hello world")时,其实就是把起始地址传给了strlen函数。

    有关这个内容,实际上是c语言的基本知识,这里只是简单的介绍下,不清楚的同学,说明你的C语言还不过关,你需要认真的
    把c语言学好。


    (5)思考3:write(fd, "hello world"+1, 10),写入文件的又是什么样的内容。




    3.2 read

    3.2.1 函数原型
    #include <unistd.h>

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

    (1)功能:从fd指向的文件中,将数据读到应用缓存buf中



    (2)参数
    1)fd:指向打开的文件
    2)buf:读取到数据后,用于存放数据的应用缓存的起始地址
    3)count:缓存大小(字节数)


    (3)返回值
    1)成功:返回读取到的字符的个数
    2)失败:返回-1,并自动将错误号设置给errno。

    数据中转的过程:
    应用缓存(buf)<————open打开文件时开辟的内核缓存<——————驱动程序的缓存<——————块设备上的文件



    3.2.2 使用read
    (1)代码演示

    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <errno.h>
    
    
    int main(void)
    {
    	int fd = 0;
    
    	fd = open("./file.txt", O_RDWR|O_CREAT, 0664);
    	if(-1 == fd)
    	{
    		printf("open fail: %d
    ", errno);
    		return 0;
    	}
    	else
    	{
    		printf("open ok
    ");
    		printf("fd = %d
    ", fd);
    	}
    
    	
    
    	char buf2[30] = {0};
    	read(fd, buf2+3, 11);
    
    	
    	printf("buf2 = %s
    ", buf2);	
    	
    
    
    	int i=0;
    	for(i=3; i<30; i++)
    	{
    		printf("%c", buf2[i]);
    	}
    	printf("
    ");
    
    
    	close(fd);
    
    
    	return 0;
    }
    

      


    (2)思考
    char buf[30];
    read(fd, buf+3, 11);

    会是什么样的效果?
    猜测的话,数据从buf[3]开始存放,一直放到buf[14]。

    验证:



    3.3.3 API调用时的正规写法
    当函数调用失败后,为了能够快速准确的排查错误,原则上来说,应该对所有的函数都进行错误处理(错误打印)。

    不过后续为了讲课的方便,除非非常必要,否则在我们写的测试代码中,就不进行错误检测了。





    3.3 0/1/2这三个文件描述符

    3.3.1 有关0/1/2

    (1)程序开始运行时,有三个文件被自动打开了,打开时分别使用了这三个文件描述符。


    (2)依次打开的三个文件分别是
    /dev/stdin,/dev/stdout,/dev/stderr。



    1)/dev/stdin:标准输入文件

    (a)程序开始运行时,默认会调用open("/dev/stdin", O_RDONLY)将其打开,返回的文件描述符是0


    (b)使用0这个文件描述符,可以从键盘输入的数据

    简单理解就是,/dev/stdin这个文件代表了键盘。


    (c)思考:read(0, buf, sizeof(buf))实现的是什么功能

    实现的是,从键盘读取数据到到缓存buf中,数据中转的过程是:

    read应用缓存buf <—————— open /dev/stdin时开辟的内核缓存 <——————键盘驱动程序的缓存 <——————键盘


    演示:





    疑问:能够像读普通文件一样读键盘吗?

    答:在Linux下,应用程序通过OS API操作底层硬件时,都是以文件形式来操作的,不管是读键盘,还是向显
    示器输出文字显示,都是以文件形式来读写的,在Linux下有句很经典的话,叫做“在Linux下一切皆文件”,说的
    就是这么个意思。



    (d)思考:为什么在我们的程序中,默认就能使用scanf函数从键盘输入数据


    我们默认就打开了代表了键盘/dev/stdin,打开后0指向这个打开的文件。

    scanf下层调用的就是read,read自动使用0来读数据,自然就可以从键盘读到数据。

    scanf("%s", sbuf) C库函数
    |
    |
    read(0, rbuf, ***)


    我们从键盘读取数据时,可以直接调用read这个系统函数来实现,也可以调用scanf C库函数来实现,
    只不过在一般情况下,我们在实际编写应用程序时,调用的更多的还是scanf 这个c库函数,原因如下:


    · 调用库函数,可以让程序能够更好的兼容不同OS,能够在不同的OS运行

    · scanf在read基础上,加入更加人性化的、更加便捷的功能,比如格式化转换


    - 直接使用read的缺点

    首先我们必须清楚,所有从键盘输入的都是字符,从键盘输入100,其实输入的是三个字符'1'、'0'、'0',
    因此使用read函数从键盘读数据时,读到的永远都是字符。

    但是如果你实际想得到的是整形数的100,你就必须自己将'1'、'0'、'0'转为整形的100,

    代码演示:




    每次使用read获取一个整形数时,你都要自己转换,如果你要输入的是浮点数的话,转起来更麻烦。

    思考:字符串形式的浮点数怎么转为真正的浮点数?
    c语言面试题中就会经常考这么一道题,“请将字符串形式的"123.45"转为真正的浮点数”,请大家自行实现。



    - scanf的优点

    scanf可以解决read的缺点,虽然scanf调用read时,从键盘读到的全部都是字符,但是你只要给scanf指定
    %d、%f等格式,scanf会自动的讲read读到的字符串形式的数据,转为整形或者浮点型数据。

    scanf("%d", &a);
    |
    |
    read(0, buf, ...);

    如果直接调用read读取,然后自己再来转换,相当于自己再实现scanf函数的基本功能。


    疑问:有了scanf函数后,read系统函数是不是没有调用的意义了,当然不是的,在有些时候,
    特别是讲到后面驱动时,有些情况还就只能使用read,不能使用scanf,学到后面就知道了。



    (e)思考:close(0)后,scanf还能工作吗?
    验证:


    为什么不能工作?
    scanf("%s", sbuf) C库函数
    |
    |
    read(0, rbuf, ***)




    2)/dev/stdout:标准输出文件


    (a)程序开始运行时,默认open("/dev/stdout", O_WRONLY)将其打开,返回的文件描述符是1

    为什么返回的是1,先打开的是/dev/stdin,把最小的0用了,剩下最小没用的是1,因此返回的肯定是1。



    (b)通过1这个文件描述符,可以将数据写(打印)到屏幕上显示

    简单理解就是,/dev/stdout这个文件代表了显示器。


    (c)思考:write(1, buf, strlen(buf))实现的是什么功能
    将buf中的数据写到屏幕上显示,数据中转的过程是:

    write应用缓存buf ——————> open /dev/stdout时开辟的内核缓存 ——————> 显示器驱动程序的缓存 ——————> 喜爱能使其


    演示:




    (d)思考:为什么在我们的程序中,默认就能使用printf打印数据到显示器

    因为程序开始运行时,就默认打开了代表了显示器/dev/stdout文件,然后1指向这个打开的文件。

    printf下层调用的是write函数,write会使用1来写数据,既然1所指文件代表的是显示器,自然就可以将数据
    写到显示器了。

    printf("*****")
    |
    |
    write(1, buf, count);



    (e)思考:如何使用write函数,将整数65输出到屏幕显示?

    · 直接输出行不行
    int a = 65;
    write(1, &a, sizeof(a));

    演示:

    为什么输出结果是A?
    人只看得懂字符,所以所有输出到屏幕显示的,都必须转成字符。

    所以我们输出时,输出的必须是文字编码,显示时会自动将文字编码翻译为字符图形。

    所以我们输出65时,65解读为A字符的ASCII编码,编码被翻译后的图形自然就是A。


    · 怎么才能打印出65

    如果要输出65,就必须将整形65转为'6'和'5',输出这两个字符才行。
    输出'6'、'5'时,其实输出的是'6'、'5'这两个字符的ASCII编码,然后会被自动的翻译为6和5这两个图形。

    总之将整形65,转为字符'6'、'5'输出即可。

    演示:


    · 为什么printf会对wirte进行封装

    - 库函数可以很好的兼容不同的OS

    - 封装时,叠加了很多的功能,比如格式化转换

    通过指定%d、%f等,自动将其换为对应的字符,然后write输出,完全不用自己来转换。


    思考:printf使用%s、%c输出字符串和字符时,还用转吗?
    其实不用转,因为要输出的本来就是字符,printf直接把字符给write就行了,当然也不是一点事情也不做,
    还是会做点小处理的。


    (f)思考:close(1),printf函数还能正常工作吗?
    验证:

    printf("*****")
    |
    |
    write(1, buf, count);


    3)/dev/stderr:标准出错输出文件

    (a)默认open("/dev/stderr", O_WRONLY)将其打开,返回的文件描述符是2


    (b)通过2这个文件描述符,可以将报错信息写(打印)到屏幕上显示

    (c)思考:write(2, buf, sizeof(buf))实现的是什么功能
    将buf中的数据写道屏幕上,数据中转的过程是:

    write应用缓存buf ——————> open /dev/stderr时开辟的内核缓存 ——————> 显示器驱动程序的缓存 ——————> 显示器

    演示:


    (d)疑问:1和2啥区别?

    · 使用这两个文件描述符,都能够把文字信息打印到屏幕。
    如果仅仅是站在打印显示的角度,其实用哪一个文件描述符都能办到。


    · 1、2:有各自的用途

    - 1:专用于打印普通信息

    - 2:专门用于打印各种报错信息

    - 使用write输出时
    + 如果你输出的是普通信息,就使用1输出。
    + 如果你输出的是报错信息,就使用2输出


    · printf和perror调用write时,使用的是什么描述符

    - printf
    printf用于输出普通信息,向下调用write时,使用的是1。
    前面已经验证过,close(1)后,printf无法正常输出。

    - perror
    perror专门用于输出报错信息的,因为输出的是报错信息,因此write使用的是2。

    验证:将2关闭,perror就会无法正常输出。


    后面讲到标准io时,还会讲到标准输入、标准输出、标准出错输出,到时候会介绍标准输出、标准出错
    输出的区别。


    4)STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO

    系统为了方便使用0/1/2,系统分别对应的给了三个宏

    0:STDIN_FILENO
    1:STDOUT_FILENO
    2:STDERR_FILENO

    可以使用三个宏,来代替使用0/1/2。

    这三个宏定义在了open或者read、write函数所需要的头文件中,只要你包含了open或者read、write的头文件,
    这三个宏就能被正常使用。


    4 lseek函数

    我们在前面的例子中,简单的用过,在这一小节,我们就来详细的介绍一下,这个lseek函数的一些具体的情况。

    先回顾下之前的例子:


    4.1 函数原型和头文件
    #include <sys/types.h>
    #include <unistd.h>

    off_t lseek(int fd, off_t offset, int whence);

    (1)功能
    调整读写的位置,就像在纸上写字时,挪动笔尖所指位置是一样的。

    C库的标准io函数里面有一个fseek函数,也是用于调整读写位置的,fseek就是对lseek系统函数封装后实现的,后面讲到标准
    io时,还会讲到fseek函数。


    (2)返回值
    · 调用成功
    返回当前读写位置相对于文件开始位置的偏移量(字节)。

    可以使用lseek函数获取文件的大小,怎么获取?
    答:将文件读写的位置移动到最末尾,然后获取返回值,这个返回值就是文件头与文件尾之间的字节数,也就是文件大小。

    · 调用失败
    返回-1,并给errno设置错误号。


    (3)参数
    off_t lseek(int fd, off_t offset, int whence);

    1)fd:文件描述符,指向打开的文件

    2)whence:
    粗定位,选项有:
    SEEK_SET:调到文件起始位置

    SEEK_CUR:调到文件当前读写的位置

    SEEK_END:调到文件末尾位置

    3)offset
    精定位:微调位置

    从whence指定的位置,向前或者向后移动指定字节数。

    为负数:向前移动指定字节数
    为正数:向后移动指定字节数

    不过当whence被指定为SEEK_SET时,如果offset被指定为负数的话,是没有意义,为什么?
    因为已经到文件头上了,在向前移动就越界了,不再当前文件的范围内了,如果非要向前调整,lseek函数会报错。


    4.2 代码演示


    od -c 文件:以字符形式查看文件内容。


    4.3 可以使用lseek制作出空洞文件
    留到下一章再来介绍。




    5. 文件描述符表

    5.1 什么是文件描述符表

    在前面的课程中,我们说当open打开文件成功后,会创建相应的结构体(数据结构),用于保存被打开文件的相关信息,

    对文件进行读写等操作时,会用到这些信息,这个数据结构就是我们要讲的“文件描述符表”。


    5.2 进程表:task_struct


    (1)前面我们介绍过task_struct结构体,这个结构体又叫进程表。

    这个结构体的成员项非常多,多达近300个。


    (2)每一个进程运行起来后,Linux系统都会为其在内存中开辟一个task_struct结构体


    (3)task_struct专门用于存放进程在运行过程中,所涉及到的所有与进程相关的信息
    其中,文件描述符表就被包含在了task_struct中。


    (4)进程运行结束后,进程表所占用的内存空间,会被释放



    5.3 task_struct 与 文件描述符表 之间的关系

    5.3.1 关系图

    图:




    (1)0/1/2默认被使用了。


    (2)文件状态标志

    1)是什么
    就是open文件时指定的O_RDONLY、O_WRONLY、O_RDWR、O_TRUNC、O_APPEND、O_CREAT、O_EXCL、O_NONBLOCK、O_ASYNC等。

    文件状态标志
    比如:fd = open("./file1.txt", O_RDWR|O_CREAT|O_EXCL, 0664);

    open打开文件成功后,会将文件状态标志保存到“文件表”中。


    2)有什么用
    读写文件时,会先检查“文件状态标志”,看看有没有操作权限,然后再去操作文件。


    比如open时指定的是:
    (a)O_RDONLY
    写(wrtie)文件时,通过fd检查“文件状态标志位”,发现只允许读,写操作会错误返回,并报“不允许写的错误”。
    演示:

    (b)O_WRONLY
    读(read)文件时,通过fd检查“文件状态标志位”,发现只允许写,读操作会错误返回,并报“不允许读的错误”。


    只有当“文件状态标志”允许相应的操作时,读写操作才能成功进行。



    (2)文件位移量 与 文件长度

    它们不是一回事,不要搞混。


    1)文件位移量:文件当前读写位置与文件开始位置的距离(字节数)。


    “文件位移量”代表的就是文件读写的位置,read、write读写数据时,通过文件位移量,就知道从哪里开始读写了。

    一般情况下打开文件时,文件的位移量默认为0,表示读写的位置在文件头上。

    每读写一个字节,文件读写位置(笔尖)就往后移动一个字节,文件位移量随之+1。


    调用lseek函数调整文件读写位置,其实就是修改文件位移量。



    2)文件长度:文件的大小
    在写文件的过程中,每写一个字节的数据到文件中,文件的长度就+1,文件长度也是动态更新的。



    (3)函数指针
    read、write等操作文件时,会根据底层具体情况的不同,调用不同的函数来实现读写,所以在V节点里面保存了
    这些不同函数的函数指针,方便调用。



    5.3.2 O_APPEND


    (1)功能

    open文件时,如果指定了这个文件状态标志,表示以追加的方式打开文件。
    写文件时,会从文件的最末尾开始写操作。


    (2)追加的实现原理
    文件的位移量代表的,就是文件的读写位置。

    open指定了O_APPEND时
    每次写文件时,都会把“文件位移量”设置为“文件的长度”,也就是说写的位置被调整到了末尾,写文件时从文件
    尾部进行追加。

    每一次写操作后,文件的内容会增加,那么自然文件的长度会被动态更新。

    总之,指定了O_APPEND后,每次写文件时,都会使用文件长度去更新文件位移量,保证每次都是从最末尾开始写数据的。



    (4)O_APPEND的意义
    多次open同一文件,实现共享操作时,指定O_APPEND可以防止数据相互覆盖的发生,后面会详细介绍。



    5.3.3 O_TRUNC
    如果文件中原来就有数据的话,open打开文件时,会全部被清空。

    由于文件已经被清空了,所以将V节点中的文件长度,修改为0


    5.4 共享操作文件

    介绍两种情况:
    · 同一进程共享操作相同的文件

    · 多个进程之间,共享操作相同文件


    5.4.1 同一进程(程序),多次open同一个文件
    (1)程序演示



    在同一个进程中多次open打开同一文件时,文件描述符可能会相同吗?
    答:不可能。

    在同一进程里面,一旦某个文件描述符被用了,在close释放之前,别人不可能使用,所以指向同一文件的描述符不可能相同。


    (2)例子中写数据时,为什么会相互的覆盖?

    1)看看共享操作时的文件描述符表长啥样
    图:





    由图知道,正是由于不同的文件描述符,各自对应一个独立的文件表,在文件表中有属于自己的“文件位移量”,开始时都是0。

    各自从0开始写,每写一个字节向后移动一个字节,他们写的位置是重叠的,因此肯定会相互的覆盖。



    2)怎么解决

    (a)指定O_APPEND即可解决

    必须每个open都要指定,有一个不指定就会覆盖,就先过马路一样,都要准守交通规则才能安全,开车的和行人,只要
    有一个不准守都会出事。




    (b)为什么使用O_APPEND可以解决

    文件长度信息时大家共享的,当文件被写入数据后,文件长度就会被更新,都指定O_APPEND后,使用不同的文件

    描述符写数据时,都会使用文件长度更新自己的文件位移量,保证每次都是在文件的最末尾写数据,就不会出现相互
    覆盖的情况。



    5.4.1 多个进程(程序),共享操作同一个文件

    (1)程序演示



    不同进程打开同一文件时,各自使用的文件描述符值可能相等,比如我们例子中的1和2进程,它们open后的描述符就相等。

    之所以相同,是因为不同的进程有自己独立的文件描述符池,都是0~1023的范围,各自分配自己的,有可能分派到相等值的
    文件描述符。


    (2)进程表 和 文件描述符表
    图:



    (3)覆盖的原因
    也是因为因为各自有独立的文件位移量。



    (4)解决办法
    同样的,指定O_APPEND标志,写操作时,使用文件长度去更新文件位移量,保证各自操作时,都在文件的尾部操作,
    就不会出现相互覆盖的情况。



    6、dup和dup2函数

    6.1 dup

    6.1.1 函数原型
    #include <unistd.h>

    int dup(int oldfd);


    (1)功能
    复制某个已经打开的文件描述符,得到一个新的描述符,这个新的描述符,也指向被复制描述符所指向的文件。

    比如:4指向了某个文件,从4复制出5,让5也指向4指向的文件。

    4 ——————> file.txt
    ^
    dup |
    |
    5 ——————————|


    至于需要用到的新描述符,dup会使用描述符池(0~1023)中当前最小没用的那一个。

    (2)返回值
    1)成功:返回复制后的新文件描述符

    2)失败:返回-1,并且errno被设置。

    (3)参数
    oldfd:会被复制的、已经存在的文件描述符。



    11.1.2 代码演示





    6.2 dup2
    6.2.1 函数原型
    #include <unistd.h>

    int dup2(int oldfd, int newfd);

    (1)功能
    功能同dup,只不过在dup2里面,我们可以自己指定新文件描述符。

    如果这个新文件描述符已经被打开了,dup2会把它给关闭后,再使用。

    比如:
    dup(2, 3);
    从2复制出3,让3也指向2所指向的文件,如果3之前被打开过了,dup2会关闭它,然后在使用。

    dup2和dup的不同之处在于:
    dup:自己到文件描述符池中找新文件描述符
    dup2:我们可以自己指定新文件描述符


    (2)返回值
    1)成功:返回复制后的新文件描述符

    2)失败:返回-1,并且errno被设置。


    (3)参数
    oldfd:会被复制的、已经存在的文件描述符。
    newfd:新的文件描述符



    6.2.2 代码演示



    6.3 dup、dup2复制的意义

    6.3.1 实现文件共享操作
    (1)代码演示:



    (2)为什么没有出现相互覆盖情况?
    图:文件描述符表





    为什么没有覆盖?
    使用dup、dup2复制方式实现文件共享时,不管复制出多少个文件描述符,它们永远只有一个文件表,所以使用所有描述符
    去操作文件时,最后使用的都是通过同一个文件位移量,不管谁操作后文件位移量都会被更新,因此不会出现覆盖。





    6.3.2 实现重定位


    (1) 什么是重定位

    某文件描述符原来指向了A文件,输出数据是输出到A文件,但是重定位后,文件描述符指向了B文件,输出时数据输出到了B文
    件,这就是重定位。

    所谓重定位,说白了就是,文件描述符所指向的文件该变了,使得数据输出的目标文件也随之变。


    (2) 举例使用dup、dup2实现重定位


    1)回顾printf与write的关系

    printf(...)
    |
    |
    write(1, buf, ...)


    printf输出时,原本通过1,将数据输出到标准输出文件(显示器)的,但是现在,我想让printf输出到file.txt文件,
    而不是输出到屏幕,应该怎么办?

    最简单的办法是就是,把printf函数调用的write函数里面的1,改成指向file.txt文件的新描述符即可,但是不幸的是
    write中的1写死了,1这个数字改不了,怎么办?

    1这个数改不了,但是可以对1进行重定位,让1不再指向标准输出文件,而是指向file.txt,printf的数据,就输出到了
    file.txt文件中。


    1)实现步骤

    (a)open file.txt文件,返回一个文件描述符,比如3

    3 ————> file.txt


    (b)close(1),不要再让1指向标准输出文件(/dev/stdout)


    (c) 使用dup、dup2把3复制到1上,让1也指向file.txt文件

    3——————>file.txt
    ^
    |
    1—————————| X ——————> /dev/stdout


    1这个文件描述符就被重定位了,凡是通过1输出的数据,都被写到了file.txt中,printf底层调用的是write(1, ...),

    用的也是1,printf的数据就被输出到了file.txt中。

    相当于printf函数的输出目的地,被重定位为了新的文件,这就是重定位。

    2)代码演示





    思考题:scanf默认从标准输入文件(键盘)读数据,请大家自行写代码对0进行重定位,重定位后,让scanf从file.txt。
    scanf(....)
    |
    |
    read(0, ...)

    (3) 什么时候会使用dup、dup2,来实现从定位

    函数中的文件描述符值写死了,无法修改为新的描述符,但是你又希望该函数,把数据输出到其它文件中,此时就可以使用
    dup、dup2对该函数中的文件描述符,进行重定位,指向新的文件,函数就会将数据输出到这个新文件。



    (4) 重定位 >
    重定位 命令(>)是dup、dup2的典型应用,这个命令在重定位时,就是调用dup、dup2函数来实现的。


    1)>使用的例子

    ls > file.txt

    ls的结果原本是输出到显示器的,但是>从定位后,就输出到了file.txt文件。


    2)具体从定位的过程
    (a)>命令(程序)会open打开file.txt文件,假设返回的文件描述符是3

    3————>file.txt


    (b)ls命令(程序)原本会调用write(1, ...),将结果输出到标准输出文件(显示器),但是 >会调用dup2,

    把3复制到1上,实现重定位,让1也指向file.txt文件,ls的结果自然就输出到file.txt文件了。

    3————>file.txt
    ^
    |
    |
    1—————| X ——————> /dev/stdout


    7. 总结:文件的共享操作

    (1)回顾单一进程多次open同一个文件,实现共享操作
    图:


    (2)多个进程多次open,共享操作同一个文件
    图:


    (3)在单个进程中,使用dup、dup2实现文件共享操作
    图:




    8. fcntl函数
    8.1 函数原型
    #include <unistd.h>
    #include <fcntl.h>

    int fcntl(int fd, int cmd, ... /* arg */ );

    不要去记我们讲的这些函数,但现在为止我也没记不住。
    我能记住的就是他们大概的用法,用到时,直接查阅man手册或者相关资料,自然就能立刻把它用起来。

    所以对于大家来说,学习这些函数的重点是于理解,而不是记住。


    (1)功能
    fcntl函数其实是File Control的缩写,通过fcntl可以设置、或者修改已打开的文件性质。


    (2)返回值
    调用成功:返回值视具体参数而定,这个后面还会再介绍

    调用失败:返回-1,并把错误号设置给errno。


    (3)参数

    int fcntl(int fd, int cmd, ... /* arg */ );

    1)fd:指向打开文件

    2)cmd:控制命令,通过指定不同的宏来修改fd所指向文件的性质。

    (a)F_DUPFD
    复制描述符,可用来用来模拟dup和dup2,后面会有例子对此用法进行说明。

    返回值:返回复制后的新文件描述

    (b)F_GETFL、F_SETFL
    获取、设置文件状态标志,比如在open时没有指定O_APPEND,可以使用fcntl函数来补设。

    返回值:返回文件的状态


    什么时候需要fcntl来补设?

    当文件描述符不是你自己open得到,而是调用别人给的函数,别人的函数去open某个文件,然后再将文件描述符返回
    给你用,在这种情况下,我们是没办法去修改被人的函数,在他调用的open函数里补加文件状态标志。

    此时就可以使用fcntl来布设了,使用fcntl补设时,你只需要知道文件描述符即可。


    c、d、e这三种情况,后面课程中具体涉及到后,再来详细介绍如何。

    (c)F_GETFD、F_SETFD

    (d)F_GETOWN、F_SETOWN

    (e)F_GETLK或F_SETLK或F_SETLKW


    8.5、测试用例


    8.5.1 fcntl 模拟dup和dup2
    不过,我们真要进行文件描述符复制时,往往都使用dup、dup2来实现,而不会使用fcntl,这里使用fcntl来模拟dup、dup2,
    完全是为了向大家演示一下,fcntl这个函数是怎么用的。

    (1)fcntl模拟dup
    代码演示:


    (2)fcntl模拟dup2
    代码演示:

    dup2进行复制时,如果新的文件描述符已经被用,dup2函数会关闭它,然后再复制,但是fcntl模拟dup2时,必须自己手动
    的调用close来关闭,然后再进行复制。



    8.5.2 fcntl补设O_APPEND文件状态标志
    代码演示:


    9. ioctl
    9.1 为什么有ioctl这个函数

    在某些特殊的情况下,使用read、write、lseek函数进行文件io(读写)操作时,存在一定的问题,此时往往就是使用ioctl
    函数,来实现这些比较特殊情况的io操作。

    ioctl是一个杂物箱,根据设置参数的不同,有很多种不同的功能,如果没有特定使用环境作为支撑的话,这个函数理解起来不太
    容易,因此在这里,我们先记住有这么一个函数,有关它的使用,等后面涉及到了具体应用环境时,再来具体讲解。

    来源:佳嵌工作室

  • 相关阅读:
    利用DTrace实时检测MySQl
    改进MySQL Order By Rand()的低效率
    RDS for MySQL查询缓存 (Query Cache) 的设置和使用
    RDS For MySQL 字符集相关说明
    RDS for MySQL 通过 mysqlbinlog 查看 binlog 乱码
    RDS for MySQL Mysqldump 常见问题和处理
    RDS for MySQL Online DDL 使用
    RDS MySQL 表上 Metadata lock 的产生和处理
    RDS for MySQL 如何使用 Percona Toolkit
    北京已成为投融资诈骗重灾区:存好骗子公司黑名单,谨防上当!
  • 原文地址:https://www.cnblogs.com/lemaden/p/10376382.html
Copyright © 2011-2022 走看看