目录
1. C语言文件操作
1.1. 一般使用
1.2. C程序的三个默认输入输出流
2. 使用文件系统调用接口
2.1. open
2.2. write
2.3. read
3. 文件描述符fd
4. 标准输入、标准输出、标准错误
5. 重定向原理
5.1. 输出重定向
5.2. 输入重定向
6. dup2接口
7. 缓冲区
8. Linux文件系统
8.1. inode
8.2. 软硬链接
1. C语言文件操作
1.1. 一般使用
回顾一下C语言中的读写文件操作:
#includeint main() { FILE* fp = fopen("./log.txt","w"); // 只写方式打开 //FILE* fp = fopen("./log.txt","r"); // 只读方式打开 //FILE* fp = fopen("./log.txt","a"); // 追加方式打开 if(fp==NULL) { perror("fopen"); return 1; } const char* str = "hello Linux!n"; int count = 8; while(count--) { fputs(str, fp); } // char buf[64]; // while(fgets(buf,sizeof(buf),fp)) // { // printf("%s",buf); // } // if(!feof(fp)) // { // printf("fgets quit not normal!n"); // } // else // { // printf("fgets quit normal!n"); // } fclose(fp); return 0; }
1.2. C程序的三个默认输入输出流
在C语言中,我们将他们分别称为:标准输入,标准输出,标准错误;他们分别对应 键盘、显示器、显示器。
例如:
#includeint main() { const char* str = "hello wrold!n"; fputs(str,stdout); // 向标准输出中打印,即打印在显示器上 fputs(str,stdout); fputs(str,stdout); }
输出重定向:本质是将stdout的内容重定向到指定文件中。
总结:上面操作向文件写入或读取,最终其实都是要访问硬件(显示器,键盘,磁盘),而OS是硬件的管理者。
所以,所以语言上对文件的操作,都必须要经过操作系统(而访问操作系统,需要通过系统调用接口,即语言上的接口都是系统调用接口封装的)。
2. 使用文件系统调用接口
2.1. open
flag:传递标志位,32bit,一个bit代表一个标志,O_WRONLY、O_RDONLY、O_CREAT等都是只有一个比特位为1的数据,且不重复。
#include#include #include #include #include int main() { umask(0); // 防止umask码&设置权限,影响期望权限 int fd = open("./log.txt",O_WRONLY|O_CREAT,0666); // 以只写方式打开,如果没有该文件就创建,权限为666 if(fd<0) // 打开失败 { perror("open"); return 1; } close(fd); return 0; }
2.2. write
功能类似C语言中的fwrite,同样是向文件中写入数据,返回值为实际写入文件数据大小。
#include#include #include #include #include #include int main() { int fd = open("./log.txt",O_WRONLY|O_CREAT,0644); if(fd<0) { perror("open"); return 1; } const char* msg = "hello world!n"; int count = 5; while(count--) { write(fd, msg,strlen(msg));// 写入时不需要写入' ',字符串以' '结束只是C语言的规定,文件中字符串不需要' ' } close(fd); return 0; }
2.3. read
read系统调用接口类似于write。
#include#include #include #include #include #include int main() { int fd = open("./log.txt",O_RDONLY); if(fd<0) { perror("open"); return 1; } char buf[1024]; ssize_t s = read(fd, buf, sizeof(buf)-1);// 将文件内容读出,需将字符串末尾加' ' if(s>0) { buf[s] = 0; printf("%sn",buf); } else { printf("read failedn"); } return 0; }
3. 文件描述符fd
open系统调用接口的返回值,打开错误返回-1,打开成功返回文件fd。
#include#include #include #include #include int main() { int fd1 = open("./log.txt",O_WRONLY|O_CREAT,0644); int fd2 = open("./log.txt",O_WRONLY|O_CREAT,0644); int fd3 = open("./log.txt",O_WRONLY|O_CREAT,0644); int fd4 = open("./log.txt",O_WRONLY|O_CREAT,0644); printf("fd: %dn",fd1); printf("fd: %dn",fd2); printf("fd: %dn",fd3); printf("fd: %dn",fd4); return 0; }
可以看到这里fd是连续的整数,那么为什么是从3开始的? 0 1 2 在哪里?
当程序运行起来之后,默认情况下,OS会默认打开三个标准输入输出流!
还记得前面说过的 标准输入、标准输出、标准错误,其实他们所对应的就是 0 、1、2
而这些从0开始的自然数,是什么,感觉像数组下标啊?没错就是数组下标!
所有的文件操作,表现上是进程执行对应的函数!而进程要对文件的操作,必须要先打开文件,就必须要将相关的属性信息加载到内存中。
操作系统中存在大量的进程,进程:打开的文件 = 1:n,而存在的大量打开的文件,操作系统会将打开的文件在内存中管理起来。
fd:本质是内核中进程和文件关联的数组的下标!
件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。
于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。
每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件
fd的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
创建子进程时,会为子进程创建PCB,所以PCB内部的 struct files_struct 也会重新创建。
而文件的struct file 不会被重新创建,因为struct file 是属于文件,文件的内容与进程无关。
4. 标准输入、标准输出、标准错误
Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2. 0,1,2对应的物理设备一般是:键盘,显示器,显示器
之所以他们是默认打开的,是因为在命令行上启动的进程都是bash的子进程,而bash是打开的,所以后续所有的进程都是默认打开的。
所以输入输出还可以采用如下方式:
#include#include #include #include #include #include int main() { char buf[64]; ssize_t s = read(0, buf, sizeof(buf)); buf[s-1] = 0; printf("%sn", buf); const char* msg = "hello Linux!n"; write(1, msg, strlen(msg)); write(2, msg, strlen(msg)); return 0; }
5. 重定向原理
5.1. 输出重定向
#include
#include
#include
#include
#include
#include
int main()
{
close(1); // 关闭标准输出
int fd = open("./log.txt", O_WRONLY|O_CREAT,0644);
// int fd = open("./log.txt", O_WRONLY|O_CREAT|O_APPEND,0644); // 追加重定向
if(fd<0)
{
perror("open");
return 1;
}
int count = 5;
while(count--)
{
printf("hello world!n");
}
return 0;
}
#include#include #include #include #include #include int main() { close(1); // 关闭标准输出 int fd = open("./log.txt", O_WRONLY|O_CREAT,0644); // int fd = open("./log.txt", O_WRONLY|O_CREAT|O_APPEND,0644); // 追加重定向 if(fd<0) { perror("open"); return 1; } int count = 5; while(count--) { printf("hello world!n"); } return 0; }
可以看到,本该打印到显示器上的hello world,却被放进了log.txt文件中。
5.2. 输入重定向
#include
#include
#include
#include
#include
#include
int main()
{
close(0); // 关闭标准输入
int fd = open("./log.txt", O_RDONLY);
printf("fd: %dn",fd);
char str[128];
while(fgets(str, sizeof(str)-1,stdin)) // 向标准输入读取数据
{
printf("%s",str); // 打印读到的内容
}
return 0;
}
原理还是类似:本应从键盘上读取数据,但是已经将标准输入关闭,换成了log.txt文件,所以这里不需要输入,直接从该文件中读取内容。
ps:语言层的文件操作接口,是通过系统调用接口封装的。
6. dup2接口
上面在理解重定向的原理时,我们会先关闭标准输入或输出,那么有没有什么方法能不关闭,而直接实现呢?
当然有!dup2函数:他的底层原理实际是将需要关闭位置的指针覆盖,换成指定fd位置的指针
例如:
#include#include #include #include #include #include int main() { int fd = open("./log.txt", O_WRONLY|O_CREAT,0644); if(fd<0) { perror("open"); return 1; } dup2(fd, 1); // 本来应该显示到显示器的内容,写入到文件中! int count = 5; while(count--) { printf("hello world!n"); } return 0; }
7. 缓冲区
上面在实现重定向时,不知道大家有没有注意一个问题,我们需要打开文件,但最后却没有关闭文件。
那关闭文件试试:
#include#include #include #include #include #include int main() { close(1); int fd = open("./log.txt", O_WRONLY|O_CREAT, 0644); if(fd<0) { perror("open"); return 1; } printf("fd: %dn", fd); fprintf(stdout, "hello world!!n"); fprintf(stdout, "hello world!!n"); fprintf(stdout, "hello world!!n"); fprintf(stdout, "hello world!!n"); fprintf(stdout, "hello world!!n"); close(fd); return 0; }
log.txt 文件中什么都没有,为什么???
画个图来解释:
当关闭fd为1位置,将新打开文件的fd置为1时,刷新策略也会由行刷新改变为全刷新!!
ps:C语言中的FILE*结构体指针内部封装了 fd 和buffer(缓冲区)。
当然也是有解决办法的,让用户缓冲区提前刷新,所用到的接口以前见过,即:fflush。
#include#include #include #include #include #include int main() { close(1); int fd = open("./log.txt", O_WRONLY|O_CREAT, 0644); if(fd<0) { perror("open"); return 1; } printf("fd: %dn", fd); fprintf(stdout, "hello world!!n"); fprintf(stdout, "hello world!!n"); fprintf(stdout, "hello world!!n"); fprintf(stdout, "hello world!!n"); fprintf(stdout, "hello world!!n"); fflush(stdout); close(fd); return 0; }
在看下面这段代码:
#include#include #include #include #include #include int main() { const char* msg = "hello write()n"; write(1, msg, strlen(msg)); printf("hello printf()n"); fprintf(stdout, "hello fprintf()n"); fputs("hello fputs()n", stdout); fork(); return 0; }
结果没问题,都输出到了显示器上。
那么如果将他重定向到 log.txt 文件中会如何?
为什么这里的printf、fprintf、fputs会被重定向两次,而write只有一次?
答案是:当重定向文件中后,缓冲策略发生变化,由行缓冲变成全缓冲,所以C语言的接口在fork之前时数据都还保存在C语言的缓冲区中,fork之后子进程发生写时拷贝,最后父子进程都将C语言缓冲区的数据刷新到系统缓冲区中,所以才有两份数据。而write接口不会在用户缓冲区存放数据,而是在fork之前直接刷新到系统缓冲区,所以文件中只有一份。
当然想让C语言接口打印的数据只有一份,也可以使用fflush提前将C缓冲区刷新到系统缓冲区。
8. Linux文件系统
8.1. inode
8.2. 软硬链接
软链接:可理解为Windows系统下的快捷方式,软连接中存放了一个文件的路径,通过软连接可直接打开该文件。
链接方式:ln -s 文件路径 软连接名
例如:
在有些情况下,如果我们想打开其他路径下的文件,每次都使用路径相对麻烦,使用软连接就比较简单。
硬链接:说直白点就是为一个文件再起名字,而原名还能用。
链接方式:ln 文件路径 硬链接名
取消链接方法:unlink 链接名
软硬链接的区别:
从图中可以看出:
软连接有自己独立的inode,软连接是一个独立文件,有自己的inode属性,也有自己的数据块(保存的是指向文件的所在路径+文件名)。
硬链接本质就不是一个独立的文件,而是一个文件名和inode编号的映射关系,因为自己没有独立的inode。



