目录
下面
前面我们已经知道什么是文件描述符了。
文件描述符实际就是数组下标。
也就是 PCB 里面有一个指针数组,里面存的都是 struct file 结构的指针,而该结构体里面保存的是一些关于文件的内容属性,所以只需要找到对应的数组下标就可以找到相关的文件,就可以对对应的文件进行操作。
而文件描述符的分配规则就是找最小的没有被分配的文件描述符。
实际上 Linux 中也是默认打开三个流,stdin、stdout、stderr。
而我们可以理解为,创建一个进程的时候,为进程的 PCB 里面的文件描述符数组的 0、1、2 设置为标准输入、标准输出和标准错误。
所以默认就是打开了三个流,而我们后续打开的进程都是从 3 开始的。
- void test2()
- {
- int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);
- if(fd < 0)
- {
- perror("open");
- exit(1);
- }
- // 打开成功
- printf("fd: %d\n", fd);
-
- const char* str = "hello fprintf\n";
- fprintf(stdout, "%s",str);
- close(fd);
- }
这里我们将 str 打印到 stdout 也就是标准输出。
结果:
- [lxy@hecs-165234 linux104]$ ./myfile
- fd: 3
- hello fprintf
这里确实打印到标准输出了,下面我们要是将 1 号文件描述符关闭,然后再打开一个文件呢:
- void test2()
- {
- close(1);
-
- int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);
- if(fd < 0)
- {
- perror("open");
- exit(1);
- }
- // 打开成功
- printf("fd: %d\n", fd);
-
- const char* str = "hello fprintf\n";
- fprintf(stdout, "%s",str);
- }
这里我们将 1 号文件描述符关闭。
然后我们打开 log.txt 文件,接着我们向标准输出中写,会发生什么呢?
结果:
- [lxy@hecs-165234 linux104]$ ./myfile
- [lxy@hecs-165234 linux104]$
- 那么我们看一下 log.txt 文件
- [lxy@hecs-165234 linux104]$ cat log.txt
- fd: 1
- hello fprintf
这里发现 fprintf 向标准输出里写的内容到了 log.txt 文件里面
但是不知道有没有发现,我上面没有关闭打开的 fd 文件(这是一个问题,后面说)。
下面我们将标准输入关闭,然后打开 log.txt 文件,然后从标准输入中读取,然后打印出来,看一下结果:
- void test3()
- {
- close(0);
- int fd = open("log.txt", O_RDONLY);
- if(fd < 0)
- {
- perror("open");
- exit(1);
- }
- // 打开成功
- printf("fd: %d\n", fd);
- char buffer[64] = {0};
- fread(buffer, sizeof(buffer) - 1, 1, stdin);
- printf("%s\n", buffer);
- }
这里关闭 0 号文件描述符(标准输入)
然后打开 log.txt 此时 log.txt 的文件描述符就是 0 号
所以此时向标准输入中读取,就会读取 log.txt 里面
结果:
- [lxy@hecs-165234 linux104]$ ./myfile
- fd: 0
- fd: 1
- hello fprintf
前面,我们关闭了标准输出,打开一个文件,此时该文件的文件描述符就是标准输出,此时向显示器上打印。那么就是向该文件里面打印
还有关闭标准输入,打开一个文件,此时该文件的文件描述符就是标准输入的位置,此时向标准输入中读取就是向该文件中读取。
那么前面的这个效果是什么呢?重定向!
但是我们看到,前面的重定向需要先关闭文件,然后才能打开,那么如果文件已经打开了,那么还可以重定向吗?
可以!
- NAME
- dup, dup2, dup3 - duplicate a file descriptor
-
- SYNOPSIS
- #include
-
- int dup(int oldfd);
- int dup2(int oldfd, int newfd);
该函数的作用就是重定向
这里的 new 是 old 的拷贝,也就是 最后都会变成 old ,所以 old 就是我们想要替换的文件描述符
如果失败的话,那么就返回 -1
下面我们使用该函数试一下:
- void test4()
- {
- int fd = open("log.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
- if(fd < 0)
- {
- perror("open");
- exit(1);
- }
- //打开成功
- printf("fd: %d\n", fd);
- //重定向
- int r = dup2(fd, 1);
- if(r == -1)
- {
- perror("dup2");
- exit(2);
- }
- //向标准输出中写入数据
- const char* str = "hello dup2";
- fprintf(stdout, "%s\n", str);
- close(fd);
- }
这里使用了 dup2 函数来重定向。
我们对 log.txt 文件描述符与标准输出的文件描述符进程重定向。
所以我们后面 fprintf 向标准输出中打印,就会打印到 log.txt 文件中。
结果:
- [lxy@hecs-165234 linux104]$ ./myfile
- fd: 3
- [lxy@hecs-165234 linux104]$ cat log.txt
- hello dup2
这里看到,即使我们最后 close 了 fd 也是可以的(这个和之前不能关闭fd都是同一个问题,这个问题先保留)
下面我们将标准输入与 log.txt 文件进行重定向,看一下是否可以从 log.txt 文件中读取到数据:
- void test5()
- {
- int fd = open("log.txt", O_RDONLY);
- if(fd < 0)
- {
- perror("open");
- exit(1);
- }
- // 打开成功
- printf("fd: %d\n", fd);
- // 重定向
- dup2(fd, 0);// 标准输入重定向
- char buffer[64] = {0};
- fread(buffer, sizeof(buffer)-1, 1, stdin);
- printf("%s", buffer);
- close(fd);
- }
这里将 log.txt 文件与标准输入重定向。
让本应从标准输入中读取到,log.txt 中读取。
然后我们打印读取到的内容。
结果:
- [lxy@hecs-165234 linux104]$ ./myfile
- fd: 3
- hello dup2
Linux 是使用C语言实现的。
那么如果我们想要访问硬件,我们怎么访问呢?
我们访问硬件可以使用 read 和 write 方法访问,也就是 I/O.
但是硬件的结构是不同的,所以访问硬件的 read/write 方法当然也是不同的!
既然方法是不同的,那么为什么我们系统只提供一个 read 和 write 方法呢?
其实可以使用多态!!!
那么C语言如何实现对象呢?如何实现运行时多态呢?
再 Linux 中,想要实现一个对象,但是C语言中只有 struct(结构体)。
我们都知道结构体里面不能放成员函数,那么如何实现呢?
函数指针!
- struct file
- {
- //成员变量
- int fd;
- ...
- //成员方法
- ssize_t (*read)(int fd, void* buf, size_t count);
- ssize_t (*write)(int fd, const void *buf, size_t count);
- }
所以我们可以写一批不同硬件的 read 和 write 方法!
我们再初始化结构体的时候,可以将对应的方法给初始化到对应的函数上。
所以这样就是可以实现对象和运行时多态。
再前面,我们提到了我们再调用printf/fprintf 等函数,有时候刷新不出来,我们现在就来解决这个问题!
其实什么是缓冲区这个问题很好回答,我们自己经常定义的一个 buffer 这样的数组,或者是临时存储数据的,都可以称为缓冲区!
所以缓冲区也就是再内存上的一段数组空间!
上面我们知道了什么是缓冲区,那么我们当然也得知道缓冲区是谁的?
那么缓冲区是谁的呢?
我们在看一下那个试验:
- void test6()
- {
- close(1);
- //
- int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
- const char* str1 = "fprintf you can see me!\n";
- const char* str2 = "write you can see me!\n";
- fprintf(stdout, "%s", str1);
- write(1, str2, strlen(str2));
- close(fd) ;
- }
这里关闭了标准输出,然后打开了 log.txt 文件。
这里到后面关闭了 log.txt 文件描述符。
结果:
- [lxy@hecs-165234 linux104]$ ./myfile
- [lxy@hecs-165234 linux104]$ cat log.txt
- write you can see me!
为什么只有 write 函数被写到 log.txt 文件里面了?
为什么 C 的 IO接口调用的函数没有写到 log.txt 文件。
那么现在就有问题了,如果这里有缓冲区的话,那么这个缓冲区是在哪里的?
显然这个缓冲区一定是在C语言上。
如果是系统上的,那么C语言的也是调用了 write 函数,那么为什么系统函数写到文件了,C语言的函数没有呢?
那么这里怎么样让他写到文件里面呢?
可以在关闭文件的时候强制刷新缓冲区!
- void test6()
- {
- close(1);
- //
- int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
- const char* str1 = "fprintf you can see me!\n";
- const char* str2 = "write you can see me!\n";
- fprintf(stdout, "%s", str1);
- write(1, str2, strlen(str2));
- fflush(stdout);
- close(fd) ;
- }
结果:
- [lxy@hecs-165234 linux104]$ cat log.txt
- write you can see me!
- fprintf you can see me!
这里强制刷新后就有数据了,那么就更能说明是C语言的!
那么为什么要有缓冲区呢?
下面举一个例子!
现在有一个卡车司机,他现在有货物需要从北京拉到上海。
那么当他现在只有一件小货物的时候,他就从北京到上海,但是由于北京到上海很远,所以每次去依次都会很麻烦,并不是因为货太多而麻烦,而是因为从北京到上海很麻烦。
所以此时卡车司机就有三种策略:
当是不重要的货物时,那么卡车司机就可以等到这一卡车装满的时候再送货。
当是比较重要的货物时,那么可以装到一半的时候,或者不到一半的时候就要去送。
当时特别重要的货物时,就需要一旦有货物就送。
实际上缓冲区也是这三种刷新策略。
立即刷新
行刷新
全缓冲
实际上,硬件的话,他们都是倾向于全缓冲的,也就是缓冲区被打满之后才刷新。
但是又由于不同的硬件需求不同,所以刷新策略是不同的。
因为不同的硬件需求不同,所以不仅要考虑效率,还要考虑体验,例如显示器,就采用行刷新。
而一般的磁盘文件而言,他们一般采用的是全缓冲。
所以这里我们也就知道了,为什么我们向 log.txt 文件里面写数据,然后我们中间关闭文件描述符他就不会写到文件里面,但是如果我们不关闭文件描述符的话,他又会刷新进去,这是如果缓冲区还有数据的话,那么当进程结束的时候也会刷新到文件里面。
但是也可以强制刷新,也就是 fflush。
下面我们继续对缓冲区理解一下:
- void test7()
- {
- int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
- if(fd < 0)
- {
- perror("open");
- exit(1);
- }
- // 打开成功
- //dup2(fd, 1);
- const char* str1 = "hello write\n";
- const char* str2 = "hello fprinte\n";
- const char* str3 = "hello fputs\n";
- const char* str4 = "hello fwrite\n";
-
- write(1, str1, strlen(str1));
- fprintf(stdout,"%s", str2);
- fputs(str3, stdout);
- fwrite(str4, strlen(str4), 1, stdout);
-
- fork();
- }
我们看这一段代码,这里我们是打开 log.txt 文件。
然后使用系统 write 函数打印一条数据。
使用C 的IO函数打印三条数据。
首先我们向显示器打印:
- [lxy@hecs-165234 linux104]$ ./myfile
- hello write
- hello fprinte
- hello fputs
- hello fwrite
然后我们重定向到 log.txt 文件里面:
- [lxy@hecs-165234 linux104]$ ./myfile > log.txt
- [lxy@hecs-165234 linux104]$ cat log.txt
- hello write
- hello fprinte
- hello fputs
- hello fwrite
- hello fprinte
- hello fputs
- hello fwrite
这里发现,系统函数打印了一条数据。
而C语言函数都打印了两条。
我们把 fork 屏蔽掉。
向显示器打印:
- [lxy@hecs-165234 linux104]$ ./myfile
- hello write
- hello fprinte
- hello fputs
- hello fwrite
重定向到 log.txt 文件:
- [lxy@hecs-165234 linux104]$ ./myfile > log.txt
- [lxy@hecs-165234 linux104]$ cat log.txt
- hello write
- hello fprinte
- hello fputs
- hello fwrite
这里看到我们关闭掉 fork j就没有刚才的现象了,说明刚才的现象确实与 fork 有关。
解释:
前面我们看到了刚才现象确实与 fork 有关。
那么我们这时候就需要想一下, fork 会做什么呢?然后导致有两份数据被刷新到了文件里面。
在 fork 的时候,我们前面的代码已经执行完了(也就是打印数据代码)。
那么显然不可能是因为创建子进程后执行了这些代码。
但是 fork 创建子进程会和父进程代码共享,而数据发生写时拷贝。
那么C语言是有缓冲区的,而且这些数据暂时还没有刷新到文件里面。
那么这些数据算不算是父进程的数据呢?
算父进程的数据!
既然算是父进程的数据,那么创建子进程的时候会不会写时拷贝呢?
会的!
既然会写时拷贝,那么不论哪一个进程在先将缓冲区中的数据刷新的时候,不就是对缓冲区中的数据发生了写入吗?
所以不就会发生写时拷贝吗?
所以此时就会有两份数据,都会被刷新到文件里面。
但是系统调用没有缓冲区(这里说的可不是内核没有缓冲区),所以就不会被写入两份。
下面我们可以写一个自己的基于系统调用的文件:
首先,我们需要一个自己的文件的结构体:
首先我们的结构体里面需要有缓冲区,可以减少IO次数,提高效率。
然后我们还要有 fd ,也就是文件描述符,因为我们一定会调用系统调用,而系统调用值认文件描述符。
由于我们有缓冲区,所以我们需要记录缓冲区的最后一个位置。
而我们缓冲区的刷新策略就是行刷新。
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
-
-
- #define NUM 128
下面我们使用的函数与宏都是来自上面的头文件和宏的定义。
- // 我的文件描述符
- typedef struct MyFILE
- {
- int _fd; // 文件描述符
- char buffer[NUM];// 缓冲区
- int end; // 缓冲区结尾位置
- } MyFILE;
这就是我们的文件描述符,里面有缓冲区,文件描述符与end.
end 这里所指的是缓冲区里面最后一个字符的下一个位置,所以也表示该缓冲区中有多少数据。
既然我们有 MyFILE 结构,所以我们也需要初始化该结构的函数:
- void InitMyFILE(MyFILE* *fd, int filed)
- {
- *fd = (MyFILE*)malloc(sizeof(MyFILE));// 为 MyFILE 申请空间
- // 初始化
- (*fd)->_fd = filed;
- memset((*fd)->buffer, 0, NUM);
- (*fd)->end = 0;
- }
我们当然也需要打开文件的一个函数:
该函数,我们模仿C语言。
其中的权限都是采用和C语言相同的逻辑。
- MyFILE* myopen(const char* path, const char* mode)
- {
- // path 和 mode 不能为空
- assert(path);
- assert(mode);
- // 查看 mode 是什么
- MyFILE* fd = NULL;
- if(strcmp(mode, "r") == 0)
- {
- int t = open(path, O_RDONLY);
- if(t >= 0)
- InitMyFILE(&fd, t);
- }
- else if(strcmp(mode, "r+") == 0)
- {
- int t = open(path, O_WRONLY | O_RDONLY);
- if(t >= 0)
- InitMyFILE(&fd, t);
- }
- else if(strcmp(mode, "w") == 0)
- {
- int t = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0666);
- if(t >= 0)
- InitMyFILE(&fd, t);
- }
- else if(strcmp(mode, "w+") == 0)
- {
- int t = open(path, O_RDONLY | O_WRONLY | O_CREAT | O_TRUNC, 0666);
- if(t >= 0)
- InitMyFILE(&fd, t);
- }
- else if(strcmp(mode, "a") == 0)
- {
- int t = open(path, O_WRONLY | O_CREAT | O_APPEND, 0666);
- if(t >= 0)
- InitMyFILE(&fd, t);
- }
- else if(strcmp(mode, "a+") == 0)
- {
- int t = open(path, O_RDONLY | O_WRONLY | O_CREAT | O_APPEND, 0666);
- if(t >= 0)
- InitMyFILE(&fd, t);
- }
- else
- {
- printf("权限出错\n");
- }
- return fd;
- }
该函数时刷新缓冲区的,当我们需要刷新缓冲区的时候我们就调用该函数,或者是当我们调用 close 的时候,我们也需要刷新缓冲区:
- void myflush(MyFILE* fd)
- {
- assert(fd);
- //将 buffer 中的数据刷新到文件
- write(fd->_fd, fd->buffer, fd->end);
- fd->end = 0;
- // 真正刷新到磁盘
- syncfs(fd->_fd);
- }
这里刷新后,就需要将 end 在置为 0,表示缓冲区已经清空了。
当我们调用完系统调用 write 之后,由于 write 没有缓冲区,所以这里会刷新到内核中。
但是内核中是由缓冲区的,所以为了真正刷新到磁盘上,我们还需要调用 syncfs 函数。
当我们关闭文件描述符的时候,我们就需要将缓冲区中的数据都刷新到磁盘,所以在 close 的时候我们需要调用 myflush.
而且刷新完后,由于我们的 MyFILE 是在堆上 malloc 的,所以我们需要释放这块空间。
- void myclose(MyFILE* fd)
- {
- assert(fd);
- //关闭之前将缓冲区中的数据刷新进磁盘
- if(fd->end)
- {
- myflush(fd);
- }
- //释放 MyFILE
- free(fd);
- }
- void test1()
- {
- MyFILE* fd = myopen("log.txt", "w");
- if(fd == NULL)
- {
- // 打开失败
- exit(1);
- }
- // 打开成功
- const char* str1 = "hello MyFILE\n";
- const char* str2 = "hello MyFILE hello str2 hello fpust";
- const char* str3 = "hello MyFILE hello world hello world hello world";
- const char* str4 = "hello MyFILE hello myclose hello myflush hello world hello nihao shijian\n";
- const char* str5 = "hello MyFILE";
- const char* str6 = "hello MyFILE\n";
- myputs(str1, fd);
- myputs(str2, fd);
- myputs(str3, fd);
- myputs(str4, fd);
- myputs(str5, fd);
- myputs(str6, fd);
-
- myflush(fd);
- myclose(fd);
- }
结果:
- [lxy@hecs-165234 linux105]$ cat log.txt
- hello MyFILE
- hello MyFILE hello str2 hello fpusthello MyFILE hello world hello world hello worldhello MyFILE hello myclose hello myflush hello world hello nihao shijian
- hello MyFILEhello MyFILE
上面由于是一个简单的模仿一下,所以并没有其他的IO函数,不过其他的IO接口也都是类似的,而且write接口跟号理解缓冲区,所以上面就以write接口示范。
有兴趣的话,可以自己实现一下其他接口。