目录
一、文件概括
二、C语言文件接口
1、打开和关闭文件
2、文件的读写
3、编写mycat
三、系统接口
四、理解Linux下一切皆文件
总结
我们对文件的操作主要是两个方面
1、对文件的内容进行操作
2、对文件的属性进行操作
文件是在磁盘上放着的,我们要访问文件,是编写代码,然后经过编译,生成可执行程序,运行程序,访问文件。对文件的访问的本质是进程通过系统接口访问文件。
要向硬件写入文件,只能通过操作系统,普通用户也想要写入,就要调用文件类的系统调用接口
而我们平时写C语言/C++所使用的文件类的函数,本质上是语言对系统接口的封装,目的是为了让接口更加好用。这就导致了不同的语言具有不同的语言级别的文件访问接口,外在表现它们是不一样的,但是它们都在内部封装了系统接口,而这样的接口一款操作系统只有一套。
如果语言不提供对文件的系统接口的封装,就会导致所有的访问文件的操作,,都要我们直接使用系统接口,一旦使用系统接口,编写文件代码,就无法在其它平台上直接运行了,就失去了跨平台性。
#include
int main()
{
FILE *fp = fopen("log.txt", "r");
if (fp == NULL)
{
perror("fopen");
return 1; /* 要返回错误代码 */
}
fclose(fp);
fp = NULL; /* 需要指向空,否则会指向原打开文件地址 */
return 0;
}
作用:用来打开一个文件
格式:FILE * fopen(const char * path, const char * mode);
它的第一个参数是文件路径,可以是绝对路径,也可以是相对路径
mode是它的选项
它主要常用的选项如上图
r是以只读的方式打开文件
w是只写的方式打开文件,如果文件不存在,它会自动创建,如果文件已经存在,它会在打开之前清空文件,然后再打开
a是append,追加,向文件的结尾追加内容
上面的特性与输出重定向和追加重定向类似
返回值:打开文件成功返回一个文件指针,若打开文件失败则返回NULL
在例子中我们使用了perror,C语言内部有errno,当某些函数出错时errno会被设置,使用perror等函数可以捕获到。
我们再回到fopen的第一个参数我们说如果不填入绝对路径,只写文件名,它会在默认路径创建文件,那么什么是默认文件呢?
我们看一段代码
1 #include
2
3 int main()
4 {
5 FILE* fp = fopen("log.txt", "w");
6 if(fp == NULL)
7 {
8 perror("fopen");
9 return 1;
10 }
11
12 fclose(fp);
13
14 return 0;
15 }
这是最普通的文件打开方式
我们发现log.txt真的在当前路径创建了,在我们编译好可执行程序,并且没有运行可执行程序时,log.txt是没有被创建的。
当我们将可执行程序移动到其它目录
我们将可执行程序移动到tmp目录下,tmp目录是一个只有可执行程序的目录,该目录是没有log。txt的,我们再运行
我们发现,生成的文件好像是在可执行程序的运行 目录下创建
我们前面了解过一种查看当前进程的方式,在/proc目录下是进程的相关信息,我们只要知道进程的pid就能够查看,所以我们稍加修改代码,在最后加上sleep死循环,并且打印他的pid
1 #include
2 #include
3 int main()
4 {
5 FILE* fp = fopen("log.txt", "w");
6 if(fp == NULL)
7 {
8 perror("fopen");
9 return 1;
10 }
11
12 printf("pid: %d\n", getpid());
13
14 while(1)
15 {
16 sleep(1);
17 }
18
19 fclose(fp);
20
21 return 0;
22 }
然后切换到/proc/27140目录下
显示更多信息
我们重点关注cwd和exe这两个文件
cwd是当前进程的工作目录
exe是可执行程序的位置
所谓的当前路径:当一个进程运行起来的时候,每一个进程都会记录自己当前所处的工作路径
我们调用fopen时的log.txt的路径是操作系统将cwd和文件名拼接而成的。
同时要注意:一个进程启动之后,cwd就一般不会改变了
fclose 函数说明:
作用:关闭一个文件流,释放文件指针
格式:int fclose( FILE *fp );
返回值:如果流成功关闭,fclose 返回 0,否则返回EOF
参数说明:
*fp:需要关闭的文件指针
注:在文件操作完成后我们应该调用该函数来关闭文件,如果不关闭文件将可能会丢失数据。因为在向文件写入数据时会先将数据输出到缓冲区,待缓冲区充满后才正式输出给文件。
文件的写入主要就三种
fwrite
fputs
fprintf
1 #include
2 #include
3 #include
4
5 int main()
6 {
7 FILE* fp = fopen("log.txt", "w");
8 if(fp == NULL)
9 {
10 perror("fopen");
11 return 1;
12 }
13
14 const char* s1 = "hello fwrite";
15 fwrite(s1, strlen(s1), 1, fp);
16
17 const char* s2 = "hello fprintf";
18 fprintf(fp, "%s\n", s2);
19
20 const char* s3 = "hello fputs";
21 fputs(s3, fp);
22
23 fclose(fp);
24
25 return 0;
26 }
成功将三个字符串写入到log.txt中,不过这里有一个细节,strlen(s1)到底要不要加上1来使C语言字符串最后的'\0'也写入到文件中?
我们可以测试一下,上面的代码只是将strlen(s1)后面加1
然后我们就发现出现了一些乱码。
这是因为文件只会保存有效的数据,C语言字符串结尾的'\0'不是有效数据,所以不能+1,如果+1就会出现乱码。
这是在系统层面上的说法:文件读取的是有效数据,如果有需要'\0'可以自行加上
基于上面的认识,我们可以自己实现一个mycat
#include
#include
#include
int main()
{
FILE* fp = fopen("log.txt", "r");
if(fp == NULL)
{
perror("fopen");
return 1;
}
char buffer[64];
memset(buffer, '\0', sizeof buffer);
while(fgets(buffer, sizeof buffer, fp) != NULL)
{
fprintf(stdout, "%s", buffer);
}
fclose(fp);
return 0;
}
在一个C语言编写的程序运行时,系统会默认打开三个标准输入输出流
stdin
stdout
stderr
它们三个与键盘显示器有关,在C语言看来,它们都被统一看作FILE*
FILE是一个结构体,它包含文件相关内容
我们前面使用了很多C语言接口
fopen fclose fread fwrite
open close read write
上面一行是C语言接口,下面一行是与之所对应的系统接口
它的第一个参数与fopen一致,就不多说了
重点是第二个参数
在官方文档中第二个选项有很多
The argument flags must include one of the following access modes: O_RDONLY, O_WRONLY, or O_RDWR. These request opening the file read-only, write-only, or read/write, respec?
这么多选项,我们重点关注几个,先来说上面的至少包含的选项
这时我们就会有疑问,他说至少包含一个选项,可是它的flags参数就只有一个,根本无法传其它选项?
答案是,它使用了位图这种数据结构,一个int具有32个比特位,并且每个选项就包含了两种,选中或者是为选中,正好与二进制相同,所以就定义int的每个比特位代表不同的选项,从而实现了一次可传递多个选项。
我们也可以写类似的代码
#include
#include
#include
#define ONE 0x1
#define TWO 0x2
#define THREE 0x4
void show(int flags)
{
if(flags & ONE) printf("Hello ONE\n");
if(flags & TWO) printf("Hello TWO\n");
if(flags & THREE) printf("Hello THREE\n");
}
int main()
{
show(ONE);
show(ONE | TWO);
show(ONE | TWO | THREE);
return 0;
}
说会正题,open的第二个参数
O_APPEND 追加写入内容,与fopen的a方式打开类似
O_CREAT 如果文件不存在就创建它
O_TRUNC 每次打开文件之前都清空文件
O_RDONLY 以只读的方式打开文件
O_WRONLY 以只写的方式打开文件
O_RDWR 以读写的方式打开文件
这证明了,在应用层看到的一个很简单的动作,在系统接口和OS层面,可能要做非常多的动作。
我们可以尝试使用这些选项
#include
#include
#include
#include
#include
#include
int main()
{
int fd = open("log.txt", O_RDONLY | O_CREAT);
if(fd < 0)
{
perror("open");
return 1;
}
const char* s1 = "Hello write\n";
close(fd);
return 0;
}
我们的选项是创建log.txt文件
我们创建出了log.txt可是却发现它的权限不对,它是乱的,并不像我们touch出来的文件权限那样
这时就要用到open的带三个参数的函数了,带两个参数的open函数主要是用来read的
三个参数的用来write,不过因为umask的存在实际的权限与我们设定的权限可能并不会相同,这时就要使用umaks函数,来手动设置当前进程的umask值
这样就成功打开了一个文件
接下来是使用write来写入文件
#include
#include
#include
int main()
{
umask(0);
int fd = open("log.txt", O_RDONLY | O_CREAT, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
fprintf(stdout, "open log.txt success fd: %d\n", fd);
const char* s = "Hello Write";
write(fd, s, strlen(s));
close(fd);
return 0;
}
最后一个接口是read,read是从一个文件中读取数据,不过这时候就需要我们手动添加'\0',因为它是系统接口它可以不考虑'\0',而调用C语言接口fgets等,他都是默认在字符串结尾添加'\0'的
#include
#include
#include
#include
#include
#include
int main()
{
umask(0);
int fd = open("log.txt", O_RDWR | O_CREAT, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
fprintf(stdout, "open log.txt success fd: %d\n", fd);
char buffer[64];
memset(buffer, '\0', sizeof buffer);
read(fd, buffer, sizeof buffer);
fprintf(stdout, "%s\n", buffer);
close(fd);
}
#include
#include
#include
#include
#include
#include
int main()
{
umask(0);
int fd = open("log.txt", O_RDWR | O_CREAT, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
int fd1 = open("log1.txt", O_CREAT | O_RDWR);
int fd2 = open("log1.txt", O_CREAT | O_RDWR);
int fd3 = open("log1.txt", O_CREAT | O_RDWR);
int fd4 = open("log1.txt", O_CREAT | O_RDWR);
printf("%d\n", fd);
printf("%d\n", fd1);
printf("%d\n", fd2);
printf("%d\n", fd3);
printf("%d\n", fd4);
close(fd);
}
我们连续打开多个文件,查看fd
发现它的fd是连续增加的可是,0,1,2去哪里了?
其实0,1,2是系统默认打开的三个文件stdin,stdout, stderr
我们可以尝试向1中写入,1是stdout,查看情况
#include
#include
#include
#include
#include
#include
int main()
{
umask(0);
int fd = open("log.txt", O_RDWR | O_CREAT, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
int fd1 = open("log1.txt", O_CREAT | O_RDWR);
int fd2 = open("log1.txt", O_CREAT | O_RDWR);
int fd3 = open("log1.txt", O_CREAT | O_RDWR);
int fd4 = open("log1.txt", O_CREAT | O_RDWR);
printf("%d\n", fd);
printf("%d\n", fd1);
printf("%d\n", fd2);
printf("%d\n", fd3);
printf("%d\n", fd4);
const char* s = "Hello World";
write(1, s, strlen(s));
close(fd);
#include
#include
#include
#include
#include
#include
int main()
{
umask(0);
int fd = open("log.txt", O_RDWR | O_CREAT, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
const char* s = "Hello World\n";
write(1, s, strlen(s));
//close(fd);
我们先将close屏蔽掉,因为这里涉及缓冲区。
我们发现向1中写入就是向stdout写入,也就是显示器。
我们当然也可以从0中读取
#include
#include
#include
#include
#include
#include
int main()
{
umask(0);
int fd = open("log.txt", O_RDWR | O_CREAT, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
char input[16];
ssize_t s = read(0, input, sizeof input);
if(s > 0)
{
input[s] = '\0';
printf("%s\n", input);
}
}
stdin, stdout,stderr内部是有fd的
#include
#include
#include
int main()
{
printf("stdin: %d\n", stdin->_fileno);
printf("stdout: %d\n", stdout->_fileno);
printf("stderr: %d\n", stderr->_fileno);
}
进程要访问文件,必须要先打开文件
一般而言 进程:打开的文件 = 1 : n
文件要被访问,前提是加载到内存中才能被直接访问
进程打开的文件是有多个的,如果是多个进程都打开自己的文件呢?
系统中会存在大量的被打开的文件,所以OS要把如此之多的文件管理起来,需要先描述在组织
在内核中,OS内部要为了管理每一个被打开的文件,需要创建struct file结构体
struct file
{
struct file* next;
struct file* prev;
//包含了一个被打开的文件的几乎所有的内容(不仅仅包含属性)
}
创建struct file对象,充当一个被打开的文件,如果有很多呢?
就使用双链表组织起来
我们调用open系统接口时
task_struct中的struct file* 指针指向struct_file,然后struct_file中的struct file* array[]这个指针数组的下标就是文件描述符fd,指针数组中存的是该进程打开的文件的被操作系统所创建的struct file结构体。struct file 结构体中包含了文件的所有信息
因此C库函数一定封装了系统调用接口,FILE*中的FILE是C语言提供的,通过FILE*找到文件描述符,将数据向文件描述符中写入
文件在Linux中被分成两种,一种是被打开的文件(被进程所打开,struct file)被叫做内存文件
另一种是没有被打开的文件(在磁盘上 文件 = 内容 + 属性)被叫做磁盘文件
文件被加载到内存,加载的内容,还是属性是分情况的
接下来重点说内存文件
struct file是一个内核的数据结构,包含了文件的所有内容(不仅仅是包含了属性)
对于图片左半部分属于进程管理
图片右半部分属于文件管理
打开的文件与进程一样都是需要被管理的struct file 然后将该进程的struct file地址填入struct file
到该进程PCB的struct file_struct中的指针数组,同时返回该位置的下标(文件描述符)
fwrite->FILE*->write->write(fd,……)->自己执行操作系统内部的write方法->能找到进程的tsk_struct->fs->files_struct->fd_array[]->struct file->内存文件
前面我们只是简单的说明了文件描述符的分配规则
最小的没有被占用的文件描述符
#include
#include
#include
#include
#include
#include
int main()
{
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
printf("fd: %d\n", fd);
printf("fd: %d\n", fd);
printf("fd: %d\n", fd);
fprintf(stdout, "Hello World\n");
const char* s = "hello";
fwrite(s, strlen(s), 1, stdout);
fflush(stdout);
close(fd);
return 0;
}
我们将1关闭,然后向stdout中打印一系列字符串
然后我们就发现,他没有打印到显示器,而是写入到log.txt中了
原因是,C语言接口十分的单纯,它将stdout写死成了数组下表为1的内容了
这样我们就实现了输出重定向
输出重定向原理
我们创建进程时,系统会给进程默认关联打开的三个文件
stdin,stdout,stderr,我们关闭了1,在C语言的FILE中1还存在,不过被设定为NULL,关闭1是将该进程与显示器的关联去掉,files_struct数组下标的内容被置为NULL,当我打开log.txt时,将1这个fd给了log.txt,将log.txt的struct file地址填入到1下标中
重定向的本质是:在OS内部更改fd对应的内容的指向
不过我们前面写的太垃圾了,系统是提供了更改关联关系的系统接口
我们只看dup2,dup2有两个参数一个oldfd,另一个是newfd
oldfd copy to newfd
换句话说最后是将newfd的内容更改为oldfd的内容
所以我们要实现输出重定向要传入的参数是dup2(3,1)
#include
#include
#include
#include
#include
#include
int main()
{
//close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
dup2(fd, 1);
printf("fd: %d\n", fd);
printf("fd: %d\n", fd);
printf("fd: %d\n", fd);
printf("fd: %d\n", fd);
fprintf(stdout, "Hello World\n");
const char* s = "hello";
fwrite(s, strlen(s), 1, stdout);
fflush(stdout);
close(fd);
return 0;
}
同样也实现了
Linux的设计哲学体现在操作系统的软件设计层面
如何使用C语言来实现面向对象和多态呢?
C语言可以使用struct来保存成员变量,C语言是无法保存成员方法的,但是我们可以使用函数指针来调用成员函数
底层不同的硬件,一定是对应不同的操作方法的,我们所接触的都是外设,所以每一个设备的核心代码都可以是read,write,I/O操作
所有的设备,都可以有自己的read和write但是代码的实现一定是不一样的。也就是说Linux从驱动向上的全部都是以面向对象的方式实现的
例如:以上就是今天要讲的内容,本文仅仅简单介绍了Linux的基础IO