文件概念
文件指的是文件内容+属性,对文件的操作无外乎就是对内容或者属性的操作
为什么平时不用文件接口
我们运行程序访问文件,本质是进程在访问文件,向硬件写入内容,只有操作系统有这个权限。普通用户想写入内容呢?所以操作系统提供了文件类的系统调用,为什么我们没有接触过
一个是因为比较难,c语言这些对接口做了封装,为了更好的使用,这也导致了不同的语言有不同的文件访问接口,但是都是对系统接口的封装。这样的os文件接口只有一套,因为os只有一个
另一个是方便跨平台,每个os都有不同的接口,一旦使用了系统接口编写代码,就无法在其他平台运行,不具备跨平台性。各类语言把平台的代码都实现一遍,条件编译,动态裁剪,这样到不同的平台都可以运行
一切皆文件(感性)
显示器也是硬件,向显示器打印也是一种写入,和磁盘写入文件没有本质区别
曾经理解的文件的操作就是read和write。而显示器的printf是一种write,键盘的scanf是一种read,站在内存的角度,都是input和output
什么叫做文件?
站在系统的角度,能够被input读取,或者能够output写出的设备叫做文件
狭义的文件:普通的磁盘文件
广义的文件:显示器,键盘,网卡,声卡,显卡,磁盘,几乎所有外设,都可以被称为文件
fopen函数
权限说明
r+和w+都为可读可写,区别为文件不在会不会创建文件
w写的方式每次都会清空文件内容,a会在原有内容后面追加
当前路径
一个w权限打开文件的程序,文件会创建在哪里
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
FILE* fd = fopen("log.txt", "w+");
if (fd == NULL)
{
perror("fopen");
return 0;
}
fclose(fd);
return 0;
}
log文件创建在了代码源文件的地方,但当把test程序放在上级目录,又会生成在上级目录里。也就是会和程序在同一个目录吗?
将程序改为死循环,进程里查找这个程序,用id找到进程目录
exe代表可执行程序在磁盘的路径
cwd是进程的当前工作路径
当一个进程启动的时候,会记录这两个路径。进程知道程序的路径,在cwd后面加上需要创建的文件,就是文件创建的路径
c语言函数
在之前学过的写文件函数中,有三个,用这些函数向log文件里写入内容
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
int fprintf(FILE *stream, const char *format, …);
int fputs(const char *s, FILE *stream);
int main()
{
FILE* fd = fopen("log.txt", "w+");
if (fd == NULL)
{
perror("fopen");
return 0;
}
char* str1 = "hello fwrite\n";
fwrite(str1, strlen(str1), 1, fd);
char* str2 = "hello fprintf\n";
fprintf(fd, "%s", str2);
char* str3 = "hello fputs\n";
fputs(str3, fd);\
fclose(fd);
return 0;
}
c语言字符串规定最后必须是\0,上面的strlen需要加1保存\0吗?
不需要,\0是c语言的规定,写入文件后,文件不需要遵守,只保存有效数据,读的时候也只读有效数据
将三个写入注释掉,只用w方式打开文件看看
文件什么内容都没有输出,w权限打开文件先清空原内容,才会写入。情况文件的功能就可以这样,w权限打开直接关闭
当打开权限改为a后,不会情况内容,会往后面追加
实现cat命令
利用main传入参数,作为打开的文件,然后用r权限读取内容打印输出
int main(int argc, char* argv[])
{
if (argc != 2)
{
printf("参数过少\n");
return 0;
}
FILE* fd = fopen(argv[1], "r");
if (fd == NULL)
{
perror("fopen");
return 0;
}
//按行读
char line[64];
while (fgets(line, sizeof(line),fd) != NULL) //fgets,string,自动加\0
{
// printf("%s", line);
fprintf(stdout, "%s", line);
}
fclose(fd);
return 0;
}
fopen 以w方式打开,默认先情况文件
fopen 以a方式打开,追加,不断向文件最后新增内容
上面的fprintf传入的stdout是标准输出流,将内容打印在显示器上,一个进程默认会打开三个流,标准输入,输出和错误,键盘和显示器都是硬件,是怎么通过FILE*写入到硬件上的
stdin
stdout
stderr
系统接口
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。
参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
返回值:
成功:新打开的文件描述符
失败:-1
这些宏其实都是一个个标志位,可以打开查看
vim /usr/include/asm-generic/fcntl.h
如何给函数传标志位
上面的全大写的参数就是宏,open函数控制打开方式的参数只有一个int,这一个标志位int就可以实现各种功能的选择。怎么样用宏控制函数的不同功能?
#define ONE 0x1
#define TWO 0x2
#define THREE 0x4
void show(int flag)
{
if (flag & ONE)
{
printf("ONE\n");
}
if (flag & TWO)
{
printf("TWO\n");
}
if (flag & THREE)
{
printf("THREE\n");
}
}
int main()
{
show(ONE);
show(TWO);
show(ONE | TWO);
show(ONE | TWO | THREE);
return 0;
}
定义了三个宏,二进制分别为只有每一位有1,show函数通过和每个标志位&判断,可以得到有没有这个参数。传入的时候想传多个参数或以下宏参数,这样就可以只用一个int传入多个标志参数,实现不同功能
返回值
为了研究open的返回值是什么意思,我们像开始一样写一个简单的打开文件,然后输出返回值
报错没有这个文件。之前的fopen没有文件会自动创建,这里的系统接口并不会。在应用层看到的一个简单的动作,在系统接口层面甚至os层面,可能需要很多动作才能完成
参数只有一个O_CREAT,文件不存在会创建,所以加上该参数
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("log.txt", O_WRONLY|O_CREAT);
if (fd < 0)
{
perror("open");
return 1;
}
printf("fd = %d", fd);
return 0;
}
文件是创建成功了,可是发现权限是随机的。这时候就需要用open函数第二个形式,加入权限的参数。第一个形式一般用来读取文件
s,表示set UID或set GID。位于user或group权限组的第三位置。如果在user权限组中设置了s位,则当文件被执行时,该文件是以文件所有者UID而不是用户UID 执行程序。如果在group权限组中设置了s位,当文件被执行时,该文件是以文件所有者GID而不是用户GID执行程序。
int fd = open(“log.txt”, O_WRONLY|O_CREAT, 0666); //rw- rw- rw-
发现权限还是和设置的不一样,这时因为有默认的umask默认掩码的存在,可以用函数设置掩码
在打开文件前先设置uamsk,设为0
返回值是3,打开成功,小于0才是失败。那么012去哪了,为什么是3
先看看打开后怎么关闭
上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。
而, open close read write lseek 都属于系统提供的接口,称之为系统调用接口
close
write
往打开的文件里写入一个字符串
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
umask(0);
int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);
if (fd < 0)
{
perror("open");
return 1;
}
char* str = "hello write\n";
write(fd, str, strlen(str));
printf("fd = %d", fd);
close(fd);
return 0;
}
再写入aa看看结果
并不像w的权限一样会清空写入,而是从开始位置覆盖
有一个选项可以清空内容
加入选项后就会清空内容
a的权限追加实际上是把情况换为APPEND
read
返回值是实际读到的字符串个数
修改代码为从文件读取内容,read读取后不会自动添加\0,需要自己添加
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
umask(0);
int fd = open("log.txt", O_RDONLY, 0666);
if (fd < 0)
{
perror("open");
return 1;
}
// char* str = "aa\n";
char buff[64];
memset(buff, '\0', sizeof(buff)); //read不会加\0
// write(fd, str, strlen(str));
read(fd, buff ,sizeof(buff));
printf("%s\n", buff);
printf("fd = %d\n", fd);
close(fd);
return 0;
}
open的返回值
认识返回值之前,回顾一下操作系统的图
文件描述符fd
创建方式打开四个文件,看看它们的返回值
int main()
{
umask(0);
int fd1 = open("log.txt", O_WRONLY|O_CREAT, 0666);
int fd2 = open("log1.txt", O_WRONLY|O_CREAT, 0666);
int fd3 = open("log2.txt", O_WRONLY|O_CREAT, 0666);
int fd4 = open("log3.txt", O_WRONLY|O_CREAT, 0666);
if (fd1 < 0)
{
perror("open");
return 1;
}
printf("fd1 = %d\n", fd1);
printf("fd2 = %d\n", fd2);
printf("fd3 = %d\n", fd3);
printf("fd4 = %d\n", fd4);
close(fd1);
close(fd2);
close(fd3);
close(fd4);
return 0;
}
返回值从3开始,依次增加。既然这样,那么0,1,2这三个去哪了。前面我们说过,进程会默认打开三个文件,stdin,stdout,stderr,类型是FILE*,这三个刚好对应这里的0,1,2
怎么证明上面的说法,可以用两次输出到标准输出的函数对比
int main()
{
fprintf(stdout, "%s", "hello fprintf\n");
write(1, "hello write\n", strlen("hello write") + 1);
return 0;
}
1也实现了输出到stdout相同的结果,所以三个标准文件刚好对应了前三个数
文件描述符就是从0开始的小整数,当打开文件时,操作系统在内存中创建相应的数据结构描述文件,有了file结构体,表示一个已经打开的文件。进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files,指向一张表file_STRUCT,该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针。所以本质上,文件描述符就是该数组的下标,拿到文件描述符,就能找到对应的文件
FILE*
fopen的返回值是FILE*,,是一个结构体,库函数内部一定调用了系统调用,系统角度只认fd,不认FILE,所以FILE内部一定对fd进行了封装
int main()
{
printf("%d\n",stdin->_fileno);
printf("%d\n", stdout->_fileno);
printf("%d\n", stderr->_fileno);
return 0;
}
打印出了stdin,stdout,stderr的三个值,正好是对应的三个文件标识符
fd是什么
进程要访问文件,必须先打开文件,一个进程可以打开多个文件,一般而言,进程:打开的文件都是1:n的。文件要被访问,就要加载到内存中,进如果多个进程都打开自己的文件呢?系统中就会存在大量被打开的文件,os要把这些文件管理起来,需要先描述,再组织。内核中构建了file的文件结构,包含了文件的属性和下一个文件的地址
用双链表将所有的文件组织了起来,那么怎么管理和访问呢?
进程的PCB结构里面保存文件的数组下标,所有文件结构的地址保存在一个数组中,open打开一个文件后先创建这个文件结构,在数组里找一个没有用的下标写入,然后返回下标,fd在内核中本质就是数组下标,进程的PCB保存了指针,指向了这个文件数组
文件fd结构
PCB里保存了files的指针,指向的结构体里保存了一个fd_array数组,类型是file的数组,这个file就保存了文件的所有属性。所以fd的本质就是fd_array的下标,通过fd就可以索引到进程的每个文件
对于一个文件的操作,系统内部其实是做了很多工作才找到了对应的文件
fd分配规则
关闭fd为0的文件,然后打开创建一个文件,输出fd看看是多少
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(0);
int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);
printf("%d\n", fd);
return 0;
}
关闭后分配的fd是0
创建文件时,每次都在文件下标数组里遍历,从0开始,找到没有用的返回,所以关掉0后返回了0
重定向
如果我们关闭1的文件,然后创建一个文件,再往标准输出打印内容呢?
int main()
{
close(1);
int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);
printf("%d\n", fd);
printf("%d\n", fd);
printf("%d\n", fd);
fprintf(stdout, "%d\n", 33);
fwrite("hello\n",6, 1, stdout);
return 0;
}
当打开文件的下标返回了1后,本来往标准输出打印的东西都打印到了文件里,这个现象就叫做重定向
进程会默认打开3个文件,再次打开文件时会在file_struct里遍历,找到没有使用的下标填入这个文件的地址,因为关闭了1下标,所以就将地址填入了1下标。linux下一切皆文件,无论是键盘还是显示器,调用库函数,向stdout打印,并不关心stdout里面是什么,只知道里面封装了文件标识符的值是1,write也只是向1里面的文件打印,所以本应该显示到屏幕里的内容却打印到了文件里
事实上重定向并不是这样先关闭再打开写入实现的,只是利用了fd分配的规则实现了功能相同的重定向
dup2系统调用
函数原型
#include <unistd.h>
int dup2(int oldfd, int newfd);
函数描述
newfd作为oldfd的拷贝,意思就是newfd会和oldfd的内容一样,用old拷贝new
这里面有oldfd和newfd,意思就是替换fd里面的内容,假设文件下面是3,想把输出重定向到3里,哪个是old,哪个是new?只需要想清楚1里面的要和3里面的一样才可以重定向,所以1就是new,3就是old
重写一下前面的重定向功能
int main(int argc, char* argv[])
{
int fd = open("log.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
if (fd < 0)
{
perror("open");
return 0;
}
dup2(fd, 1);
// printf("hello dup2\n");
fprintf(stdout, "%s\n", "hello dup2");
//close(fd);
return 0;
}
将内容重定向输入到log.txt文件中
虚拟文件系统
虚拟文件系统(VFS)是由Sun microsystems公司在定义网络文件系统(NFS)时创造的。它是一种用于网络环境的分布式文件系统,是允许和操作系统使用不同的文件系统实现的接口。
再进一步理解一切皆文件,这时linux设计的哲学,体现在操作系统的软件设计层面
linux是用c语言写的,如何用c语言实现面向对象,甚至运行时多态
我们都知道类有成员属性和方法,struct可以保存属性,那怎么保存方法。可以利用函数指针来指针一个个功能函数
在驱动层面,底层不同的 硬件都是外设。对应不同的操作方法,每一个设备的核心访问函数,都可以是read,write,也就是I/O。所有的设备都有自己的read和write,但是,代码的实现一定是不一样的。文件结构里只需要保存函数指针就可以指向不同设备的功能,这就是Os和驱动的耦合
在linux的文件结构里,有一个结构体指针,指向的结构里面保存很多功能函数的函数指针,用来指向和调用不同的文件功能。这就是一切皆文件的设计和管理模式