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

    来源:佳嵌工作室

  • 相关阅读:
    UVA10740 Not the Best (K短路)
    UVA10967 The Great Escape(最短路)
    UVA 10841 Lift Hopping in the Real World(dijkstra)
    U盘启动的PE系统的制作方法
    让远程桌面支持多用户
    学习的书的下载地址
    刚安装完的vs2008写的ajax应用提示sys未定义
    AS3 Libs
    禁用触发器
    Microsoft .NET 类库开发的设计准则
  • 原文地址:https://www.cnblogs.com/lemaden/p/10376382.html
Copyright © 2011-2022 走看看