文件 = 内容 + 属性
文件分为打开的文件和没打开的文件。
打开的文件:文件是谁打开的 --- 进程,学习文件,本质上是学习 进程和文件的关系
没打开的文件: 在哪里放着 --- 磁盘,我们最关注什么问题呢? 没有被打开的文件非常多,也就是磁盘上的文件非常多,这些文件有序的放在目录里。文件时如何有序的放置好的呢? -- 我们要快速的对文件进行增删查改,在此之前,要先找到文件。
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: 追加写
学习C语言的时候,在使用文件操作的时候,如果文件不存在,会自动的在当前路径下创建一个文件。
#include <stdio.h>
int main()
{
FILE *fd = fopen("1.txt", "w");
if (fd == NULL)
{
perror("fopen");
return -1;
}
fclose(fd);
return 0;
}
系统是如何找到当前路径的,在执行程序的时候,系统会在一个文件中存储该程序的信息。
ls /proc/pid可以查看对应进程号的信息。
cwd就是当前路径 --- 如果更改了当前进程的cwd,就可以把文件新建到其他目录
#include <stdio.h>
#include <unistd.h>
int main()
{
chdir("/home/sxk");
FILE *fd = fopen("1.txt", "w");
if (fd == NULL)
{
perror("fopen");
return -1;
}
fclose(fd);
printf("%d\n", getpid());
sleep(100);
return 0;
}
文件确实在sxk目录下。这样就可以把文件创建到其他目录中。
在用代码把内容写入到文件的时候,w是在文件的开头写入,第一次写入 hello world,之后,在写入abcd,为什么hello world都没了,为什么不会是替换前四个字符 --- w 写入之前,都会对文件进行清空处理。
在回想一下重定向,
也是先清空文件内容,只要把文件打开了,就会先清空,然后再进行写入。
再使用C语言写入文件的时候,strlen (hello world)的长度是11,最后的 字符串结尾标志 ‘\0’要写入吗? --- 不需要,这个结尾标志是C语言的结尾标志,跟文件没什么关系。
还有一种写入方式就是 a,以追加的形式进行写入。
曾经说过,Linux中一切皆文件,我们电脑中的磁盘,网卡,显示器,键盘等外设,其实都有相应的结构体,结构体中有对应的属性,指针等,然后通过结构体指针以数据结构的形式将其关联起来。通过对文件的管理来实现对硬件的管理。
C程序默认再启动的时候,会打开三个文件
STDIN 键盘文件
STDOUT 显示器文件
STDERROR 显示器文件
文件其实是在磁盘上的,磁盘是外部设备,访问磁盘文件其实是访问硬件。
要访问硬件就必须通过系统调用,C语言中的printf,fprintf库函数,都是封装后的系统调用接口,操作系统不相信任何人,它只会放开一些系统调用接口供使用。
通过 man 2 open可以查看打开文件的接口
第一个参数:要打开文件的文件名
第二个参数:要打开文件的模式
第三个参数:当你想创建一个文件的时候,该文件的权限。
手册里,关于flage的选项也挺多的。
都是大写的,他们都是宏,是二进制序列。比如: 001 代表读, 010 代表写 那么 001 | 010 == 011 代表 读写。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#define ONE (1 << 1)
#define TWO (1 << 1)
#define THREE (1 << 1)
#define FOUR (1 << 1)
void show(int flags)
{
if (flags & ONE) printf("function1\n");
if (flags & TWO) printf("function2\n");
if (flags & THREE) printf("function3\n");
if (flags & FOUR) printf("function4\n");
}
int main()
{
show(ONE);
show(TWO);
show(THREE);
show(FOUR);
return 0;
}
比如这个代码,通过宏,可以看它的二进制,然后表述打开文件的方式。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
int main()
{
int fd = open("log.txt", O_WRONLY);
int fd = open("log.txt", O_WRONLY | O_CREAR);
int fd = open("log.txt", O_WRONLY | O_CREAR, 0666);
if (fd < 0)
{
perror("open");
return -1;
}
close(fd);
return 0;
}
O_WRONDLY并不会以创建文件的形式打开文件。可以把 O_CREAT添上去。
再运行,会发现,这个文件
这个文件权限不对
这是因为,再创建文件的时候,要告诉系统,这个文件的权限是什么。用的是八进制,改完之后重新make一下。
也可以用 umask来修改掩码。操作系统中有一个umask,代码中也有一个umask,那么创建文件的时候应该听谁的?就近原则,并且代码中的并不会影响系统中的。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#define ONE (1 << 1)
#define TWO (1 << 1)
#define THREE (1 << 1)
#define FOUR (1 << 1)
int main()
{
umask(0);
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
if (fd < 0)
{
perror("open");
return -1;
}
close(fd);
return 0;
}
运行代码
此时就可以看到文件的权限变成了666 。
open的返回值是一个整数,file descriptor: 文件描述符 fd,后续对文件的操作靠这个整数就可以完成。
close 是关闭文件,把open的返回值当成参数写造close中即可。
write就是写文件,
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#define ONE (1 << 1)
#define TWO (1 << 1)
#define THREE (1 << 1)
#define FOUR (1 << 1)
int main()
{
umask(0);
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
if (fd < 0)
{
perror("open");
return -1;
}
const char* msg = "hello world";
write(fd, msg, strlen(msg));
close(fd);
return 0;
}
那么 要再strlen (msg) + 1吗? 不需要,只需要把文件的有效内容写入进去就行了。
现在将msg中的字符串改为 123, 但是并不是跟C语言中的w权限一样,覆盖写,这个是覆盖似的往文件中写入。
可以再open中添加上一个权限, 0_TRUNC,每次打开文件的时候先将文件清空。
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
还有一个追加写,0_APPEND,追加写是和清空写是冲突的。先把清空写删了,再弄追加写。
int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
C语言中的文件操作是库函数,上面用的open,write,close等是系统调用接口。
像Py,C++,JAVA,只要再Linux中用文件操作运行,原来都是一样的。
前面说过,被打开的文件一定会被操作系统管理起来,进程把文件打开,操作系统会创建该进程的PCB,而文件打开之后,操作系统会创建出一个 struct file对象。直接或简介包含 一些属性如(再磁盘的什么位置,基本属性,权限,大小,读写位置,谁打开的,文件的内核缓冲区信息,struct file *next指针)。文件打开的多了之后,会以双链表的形式进行链接,管理文件就变成了以双链表的增删查改。
进程如何和该进程打开的文件建立关系的呢? 进程创建之后系统会创建PCB,PCB中会存在指针(struct file_struct *files),通过指针来找到打开的文件。
还有一个系统调用接口叫做 read,可以把文件中的内容读出来。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
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)
{
perror("read");
return -1;
}
buf[s] = '\0'; // 写入文件的时候,不需要把 \0写入进去,但是输出回来的时候需要加上\0。
printf("%s\n", buf);
return 0;
}
查看一下文件描述符
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#define filename "log.txt"
int main()
{
int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
if (fd < 0)
{
perror("open");
return -1;
}
printf("fd:[%d]\n", fd);
const char* msg = "HELLO WORLD\n";
int cnt = 5;
while (cnt)
{
write(fd, msg, strlen(msg));
cnt--;
}
}
// 输出结果 是 3
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#define filename "log.txt"
int main()
{
close(0);
int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
if (fd < 0)
{
perror("open");
return -1;
}
printf("fd:[%d]\n", fd);
const char* msg = "HELLO WORLD\n";
int cnt = 5;
while (cnt)
{
write(fd, msg, strlen(msg));
cnt--;
}
}
// 输出结果 是 0
如果关闭的不是0,而是1,再次运行程序,就不输出了。但是文件内容却写进去了。
如果关闭的是2,输出的结果就变成了 2。
关0,默认打开的新文件所返回的文件描述符就是0,关2返回2.
文件描述符对应的分配规则是从下标0开始,寻找最小的没有使用的数组位置,它的下标就是新文件的文件描述符。
如果我把循环中的写文件操作改为往文件描述符为1号的文件中写,执行程序后,会直接输出5个HELLO WORLD,如果我先关闭1号,再往1号中去写内容。执行之后发现,本来应该输出再显示器上的内容,却直接打印到了log.txt中。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#define filename "log.txt"
int main()
{
close(1);
int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
if (fd < 0)
{
perror("open");
return -1;
}
printf("fd:[%d]\n", fd);
const char* msg = "HELLO WORLD\n";
int cnt = 5;
while (cnt)
{
write(1, msg, strlen(msg));
cnt--;
}
return 0;
}
先把1号关了,再打开log.txt文件,fd就变成了1号,while循环中的写文件操作也就变成了往log.txt中写了。
这是不是就是重定向?将要写入到显示器的内容重定向到log.txt文件中。
那么有没有一种函数,可以实现不显示的关闭文件,但是要完成重定向操作?
dup2函数可以完成。
oldfd原来的文件描述符,newfd 复制成新的文件描述符。
使newfd成为oldfd的副本,两个文件描述符指向同一个文件
假设newfd已经指向了一个文件描述符,首先close原来打开的文件,然后newfd指向oldfd指向的文件。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#define filename "log.txt"
int main()
{
int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
if (fd < 0)
{
perror("open");
return -1;
}
dup2(fd, 1);
close(fd);
printf("fd:[%d]\n", fd);
const char* msg = "HELLO WORLD\n";
int cnt = 5;
while (cnt)
{
write(1, msg, strlen(msg));
cnt--;
}
}
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
const char *fstr = "hello fwrite\n";
const char *str = "hello write\n";
// C语言提供的
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
fwrite(fstr, strlen(fstr), 1, stdout);
// 操作系统提供的
write(1, str, strlen(str));
return 0;
}
这个代码运行之后是这样的结果。
这个结果也符合预期。
把运行结果重定向到log.txt中后,查看该文件,得到的结果也符合预期。
现在把系统提供的接口write给删了,只留C语言提供的库函数,并在结尾加上close(1);
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
const char *fstr = "hello fwrite\n";
const char *str = "hello write\n";
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
fwrite(fstr, strlen(fstr), 1, stdout);
close(1);
// write(1, str, strlen(str));
return 0;
}
结果也符合预期
如果再结尾添加上fork
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
const char *fstr = "hello fwrite\n";
const char *str = "hello write\n";
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
fwrite(fstr, strlen(fstr), 1, stdout);
// close(1);
write(1, str, strlen(str));
fork();
return 0;
}
现在将代码运行结果重定向到log.txt文件中后,再查看文件内存。
结果和上面结果就不一样了。C语言中的库函数调用了两次。 但肯定和fork有关系。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
const char *fstr = "hello fwrite";
// const char *str = "hello write\n";
printf("hello printf");
fprintf(stdout, "hello fprintf");
fwrite(fstr, strlen(fstr), 1, stdout);
close(1);
// write(1, str, strlen(str));
// fork();
return 0;
}
现在将系统调用函数和fork给注释掉,再将所有字符串中的换行符给注释掉。
运行,发现,没有任何显示。
再执行完写入代码之后,才关闭的文件,为什么没有打印??
上面用的fprintf,fputs等C函数,底层都调用了write,其实都写入了,只不过是写入到了缓冲区当中。
只不过,这个缓冲区一定不在操作系统内。如果写入到了操作系统当中,就已经写入到了系统当中,再调用close的时候,就可以刷新缓冲区,将内容输出出来。C语言会给我们提供一个缓冲区,这个缓冲区是用户级的缓冲区。等到何时的时候,系统会自动的将C缓冲区自动的写入到系统缓冲区当中。再close的时候,缓冲区并没有内容,所以关闭之后,不会显示出来任何内容。
那为什么上图中加了换行符能输出出来?
显示器的刷新方案是行刷新。再printf执行完之后,遇到了换行符的时候,就会立即将数据刷新出去。
刷新的本质就是将数据通过write写入到内核当中。
目前我们认为:只要将数据刷新到了内核,数据就到达硬件了。
1.缓冲区刷新问题
无缓冲---直接刷新
行缓冲---不刷新,直到遇到换行符
全缓冲---缓冲区满了,才刷新
2.为什么要有这个缓冲区
解决效率问题---用户的效率问题
配合格式化
为什么要有这个C缓冲区
1.解决效率问题,用户效率问题。
2.配合格式化。
这个缓冲区在哪里
FILE里面有对应打开文件的缓冲区字段和维护信息。
这个FILE对象属于用户呢?还是属于操作系统呢?这个缓冲区是不是用户级的缓冲区呢?
不属于操作系统。是用户级缓冲区。
现在来解决一下上面重定向到文件,却出现了7条信息的问题。
向文件打印,缓冲方案变成了全缓冲。
遇到换行符不在刷新,而是等缓冲区域写满才刷新。
代码执行到fork之后,子进程会以写时拷贝的形式父子进程各自私有一份,那么这个用户级别的缓冲区,父子进程也是各自私有一份,这条消息就变成了两份,所以C接口的函数会打印出来两次。
FILE中的缓冲区意义是什么?
如果一次一次的写入到系统缓冲区中太浪费时间,不如一次性写入大量数据。把C语言调用该函数的速度更快。
通过 ln -s 文件名A 要指向A的文件名
可以创建一个软链接
软链接创建的文件是一个独立的文件。具有独立的文件
通过 ln 文件名A 要指向A的文件名
可以创建一个硬链接
通过 ls -li可以发现,软链接的inode不同,但是硬链接的inode却相同
所谓的硬链接其实就是 再特定的目录的数据块中新增文件名和指向的文件的inode编号的映射关系。
软链接是一个独立的文件,有独立的inode,它的数据块里面保存的是指向文件的路径。就跟windows中的快捷方式一样,我们再桌面点击快捷方式,可以运行程序。