标准输入、输出和标准错误
#include <stdio.h>
#include <string.h>
int main()
{
FILE *fp = fopen("myfile", "w");
if(!fp){
printf("fopen error!\n");
}
const char *msg = "hello bit!\n";
int count = 5;
while(count--){
fwrite(msg, strlen(msg), 1, fp);
}
fclose(fp);
return 0;
}
回顾这个代码以及输出结果:
上图中fwrite(msg, strlen(msg), 1, fp) ,其输出流选择的是文件指针指向的文件myfile ,所以myfile 的内容被写入进去了。
思考:
为什么使用printf和scanf能够分别打印内容以及接受内容?
其实C进程执行的时候会默认打开三个流,即标准输入(stdin) ,标准输出(stdout) 和标准错误(stderr)
如果我们将文件指针fp改成stdout,那么程序执行后,内容将被写道标准输出上,即屏幕。
stdin,stdout和stderr也是文件,因为形参的位置就是一个文件指针
- 一个命令或程序,按下回车键后,要么会显示程序运行的结果,要么会显示状态和错误信息。
- 以ls为例,当按下ls命令后,它会把其运行结果发送到一个称为标准输出(stdout) 的特殊文件中。
- 状态信息则会发送到一个称为 标准错误(stderr) 的文件中。
- 命令是通过键盘输入给电脑的,这个键盘叫做的标准输入(stdin)
- I/O重定向功能可以改变输出内容的发送目的(也就是不让你发送到屏幕上),也可以改变输入内容的来源地(也就是说甚至可以来自于文件)
系统调用接口(write、read)
- write:写入数据
- read:读取数据
- open:打开文件
- close:关闭文件
以上都是针对于系统层面而非用户层面。
库函数和系统调用接口的关系
C语言中的例如fwrite,fread等相关函数可能都比较熟悉,而再系统层面的write、read可能用的较为少。但其实他们之间的关系就是系统调用接口与用户操作接口之间的关系。
从开发角度上看,操作系统会对外表现为一个整体,但是会暴露自己的部分接口,以供上层开发者使用——系统调用。但是这样也带来了一个麻烦,因为系统层面的知识多而繁杂导致使用成本过高。所以一些开发者其实会将部分系统进行适度开发、封装来提供给用户使用,这也是库函数由来的原因
说的简单点,就是fwrite函数是建立在系统函数write之上,系统函数write只有一个,而相关的库函数可能有很多都是建立再write等函数而二次开发的。
open函数
函数原型以及头文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char* pathname,int flags);
int open(const char* pathname,int flags,mode_t mode);
-
pathname: 要打开或创建的目标文件 -
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。 -
flags参数: -
O_RDONLY : 只读打开 -
O_RDWR : 读,写打开 -
O_WRONLY : 只写打开(上述三个只能存在一个) -
O_TRUNC :刷新重写 -
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限 -
O_APPEND : 追加写 -
返回值:成功:新打开的文件描述符 失败:-1
PS:open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open。(一般设为0644)
close函数
函数原型以及头文件
include <unistd.h>
int close(int fd)
read函数
函数原型以及头文件
include <unistd.h>
ssize_t read(int fd,void* buffer,size_t count);
fd :文件描述符buffer :读取的内容会送入这个缓冲区count :读取文件的大小- 返回值:正常情况:返回读取到的字节数 读到EOF:返回0 异常情况:返回-1
write函数
include <unistd.h>
ssize_t write(int fd,const void* buffer,size_t count);
- buffer:数据来源
- count:你期望写入的字节数
- 返回值:成功:返回成功写入的字节数 失败:返回-1
具体案例
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main()
{
int fd = open("log.txt", O_CREAT | O_WRONLY, 0644);
if(fd < 0)
{
perror("open failed : ");
return 1;
}
const char* str = "hello world";
write(fd, str, strlen(str));
close(fd);
return 0;
}
文件描述符
先看程序
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
int fd1=open("log1.txt",O_WRONLY);
int fd2=open("log2.txt",O_WRONLY|O_CREAT);
int fd3=open("log3.txt",O_WRONLY|O_CREAT);
int fd4=open("log4.txt",O_WRONLY|O_CREAT);
int fd5=open("log5.txt",O_WRONLY);
printf("%d\n",fd1);
printf("%d\n",fd2);
printf("%d\n",fd3);
printf("%d\n",fd4);
printf("%d\n",fd5);
}
1、5文件由于没有使用O_CREAT 而打开失败,所以返回值为-1,第2-4个文件打开成功,所对应的文件描述符就是整形数值,对应为3-5。
文件描述符(File descriptor):从系统层面讲就是一个个的整数,整数的范围是0-N ,你也许好奇为什么上述案例中第一个正常打开的文件的描述符是从3开始的,0,1,2呢?其实0,1,2就是标准输入,标准和输出和标准错误。
那么如果一上来就关闭1号文件呢,也就是一开始就把标准输出关闭。这样在创建文件时,新产生的文件就会被分配较低的文件描述符,也就是1号。而1号本该是输出到屏幕的,但是却输出到了文件当中去——这其实就是重定向的本质
系统如何管理文件
操作系统遵循的管理原则就是“先描述,再组织”,操作系统管理文件时依旧遵循这样的原则,文件描述符的数值其实有一个特点,就是起始是从0开始的,这和数组下标有相似之处。
当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files,指向一张表files_struct,该表最重要的部分就是包涵一个指针数每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。
Linux一切皆文件
Linux下一切皆文件,这句话自学习Linux时就被不断提及,但是很多人对于这个概念还是模棱两可。最主要的一点就是像硬盘,屏幕这类硬件为什么可以当做文件去对待?
Linux是由C语言编写完成的,C语言是一门面向过程的编程语言,不支持面向对象。尤其体现在其结构体中不能有成员函数,不支持面向对象不代表不能编写面向对象的代码。C语言中有一个非常强大的神器——函数指针。
对于屏幕来说,屏幕之上有让其正确执行功能驱动代码,将显示器封装为结构体,使用函数指针指向驱动代码,这样不管是硬盘,还是键盘又或者是屏幕,从用户的角度上讲,他们都一样,都是文件,这也是一切皆文件的含义
源代码验证
上面的叙述都停留在理论角度,下面的是Linux内核源代码,也证实了这些
首先task_struct 里有struct files_struct* files ,这样的一个结构体指针,指向一个结构体。
接着files_struct 中保存了一个指针数组,每个指针指向一个文件结构
每个文件结构体里保存了文件的一些基本信息
值得注意的是有一个const struct file_operations* f_op ,这个结构体里全是函数指针——一切皆文件
FILE案例
在学习C语言的时候,我们接触过printf 函数,它的结果会输出到屏幕上,还有一个fprintf ,他可以指定输出流输出,如果指定为了stdout ,则也表示输出到屏幕上
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main()
{
close(1);
int fd = open("log.txt", O_CREAT | O_APPEND | O_WRONLY, 0644);
if(fd < 0)
{
perror("open failed");
return 1;
}
const char* str = "hello world by write\n";
const char* str1 = "hello world by write\n";
const char* str2 = "hello world by write\n";
write(1, str, strlen(str));
printf("%s",str1);
fprintf(stdout,"%s",str2);
fflush(stdout);
close(fd);
return 0;
}
close(1)之后,fd描述符变为1,行缓冲变为全缓冲,需要使用fflush(stdout)刷新fprintf与printf,而write没有缓冲,可被直接打印出来,但是内容全部写到了log.txt当中。这是因为重定向的原因,1变成了fd的描述符。
printf是一个库函数,会向stdout中输出,stdout本质也是一个文件指针
其指向的结构体就是C语言中FILE
这个结构体中的_fileno其实就是文件描述符,而stdout指向的FILE结构体里的这个_fileno默认就是1,所以这才会出现关闭1号文件后,printf函数也会将其内容输入到log.txt中的现象
|