1. 文件
狭义上的文件:磁盘上的文件。
广义上的文件:显示器,网卡,键盘...。
站在系统的角度,只要能够被输入读取,或者能被输入写出的设备都叫做文件。
Linux认为一切皆文件。访问文件,本质上是进程在访问文件。
2. 文件相关的C语言接口
2.1打开文件
fopen
FILE*fopen(const char*path,const char*mode)
FILE*fp=fopen("makefile,c","r");
path为文件名的路径 mode是访问文件的模式
模式 | 描述 | 文件是否存在 |
"r" | 打开文件读取 | 存在 |
"r+" | 打开文件读取并写入 | 存在 |
"w" | 创建文件并写入 | 已存在就先清空 |
"w+" | 创建文件读取并写入 | 已存在就先清空 |
"a" | 创建文件并附加写入 | 已存在不清空 |
"a+" | 打开文件读取附加写入 | 已存在不清空 |
2.2 创建文件的路径
当打开文件操作需要创建文件时,该文件会在当前路径中生成。
Linux当前路径:当进程运行起来的时候,每个进程都会记录自己当前所处的工作路径。
查看方法:拿到进程的pid后
ls/proc/14613-l
可以看到exe和cwd cwd即当前路径
2.3 清空文件
FILE*fp=fopen("makefile.c","w");
fclose(fp);
或者命名行中
>makefile.c
2.4 文件关闭
int fclose(FILE * stream)
fclose(fp);
2.5 文件写入
fwrite
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
const char*s1="hello fwrite";
fwrite(s1,strlen(s1),1,fp);
参数说明 :
const void *ptr : 指针指向要写出数据的内存首地址 ;
size_t size : 要写出数据的 基本单元 的字节大小 , 写出单位的大小 ;
size_t nmemb : 要写出数据的 基本单元 的个数 ;
FILE *stream : 打开的文件指针。
注:C语言中字符串以'\0'结尾,这里传入strlen(s1)却不用+1。因为文件不需要遵守C语言的规定,文件仅保存有效数据。
fprintf
int fprintf( FILE *stream, const char *format, ... ); const char*s2="hello fprintf"; fprintf(fp,"%s",s2);//s2以%s的形式输入fp char name[20] = "Mary"; fprintf( fp, "Hello %s\n", name );//Hello Mary
fputs
int fputs(const char *str, FILE *stream)
const char*s3="hello fputs";
fputs(s3,fp);
2.6 文件读取
fgets
char *fgets(char *s, int size, FILE*stream);
char line[64];//存放的地方
fgets(line,sizeof(line),fp);
从 stream 流中读取 size 个字符存储到字符指针变量 s 所指向的内存空间。
3. 文件操作的系统接口
C语言中使用文件操作,默认打开三个标准输入输出流。stdin,stdout,stderr.但这些流的类型是FILE*
C语言库 | fopen | fwrite | fread | fclose |
系统接口 | open | write | read | cols |
3.1 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)
打开成功返回非负数,失败返回-1
umask(0);//将该进程的umask值置0
int fd=open("makefile",O_WRONLY|O_CREAT|O_TRUNC,0666);//rw-rw-rw-
参数说明
flags :一些宏定义。相当于传参,形式是 宏A|宏B|宏C
mode:用八进制代表的权限。 注:这里最后的文件权限会被os的umask值影响。
相关宏:
宏 | O_WRONLY | O_CREAT | O_TRUNC | O_APPEND | O_RDONLY | O_RDWR |
功能 | 只写 | 没有该文件创建 | 打开就清空 | 追加内容 | 只读 | 可读可写 |
3.2 close
作用:关闭文件 头文件 <unistd.h> 声明 int close(int fd)
close(fd);
注意:调用close(fd)就会直接关闭文件,而不会先让缓冲区内的数据刷新出来。
3.3 write
作用:写入 头文件 <sys/types.h> <sys/stat.h> <fcntl.h>
const char*s="hello write";
write(fd,s,strlen(s));//这里给的是元素个数。
这里是清空并从头写入还是追加,具体要看open传入的参数
返回值:写入成功返回写入的字节数。失败返回-1。
3.4 read
作用:读文件到某地方 头文件 <unistd.h>
声明:ssize_t read(int fd,void *buf,size_t count);
char*buffer[64];
memet(buffer,'\0');
read(fd,buffer,sizeof(buffer));//这里给的是字节大小
从fd读取字节大小为sizeof(buffer)的数据放到 buffer
返回值:读到的字节数,有可能小于count。
4. 文件描述符
fd是什么,在文件中仅打开一次文件,打印fd的值为3。
int main()
{
umask(0);
int fd1=open("make_my_file.c",O_WRONLY | O_CREAT | O_TRUNC,0666);
printf("open success,fd:%d\n",fd1);
return 0;
}
每多打开一次,文件描述符的值就多加1。0、1、2呢?
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
umask(0);
int fd1 = open("log1.txt", O_WRONLY|O_CREAT|O_APPEND, 0666); //rw-rw-rw-
printf("open success, fd: %d\n", fd1);
int fd2 = open("log2.txt", O_WRONLY|O_CREAT|O_APPEND, 0666); //rw-rw-rw-
printf("open success, fd: %d\n", fd2);
int fd3 = open("log3.txt", O_WRONLY|O_CREAT|O_APPEND, 0666); //rw-rw-rw-
printf("open success, fd: %d\n", fd3);
int fd4 = open("log4.txt", O_WRONLY|O_CREAT|O_APPEND, 0666); //rw-rw-rw-
printf("open success, fd: %d\n", fd4);
close(fd1);
close(fd2);
close(fd3);
close(fd4);
return 0;
}
4.1 fd的本质
数组下标!
在C语言中使用文件操作,默认打开三个标准输入输出流,stdin,stdout,stderr这三个的类型是FILE*类型,FILE是C语言中的一种自定义结构体,针对于系统层面,系统不认识FILE,所以在stdin,stdout,stderr中一定封装了0、1、2,其fd值即是对应的0,1,2。
证明:
printf("stdin:%d",stdin->fileno);//0
进程要访问文件,必须要现打开文件,文件被加载到内存中才能被访问。
进程:其打开的文件=1:n 系统为了管理庞大的文件,构建了struct file{}结构体,一个文件对应一个结构体(内容类似双链表),在用一个FILE*的数组存放这些文件的地址,以便管理。
4.2 fd的分配规则
如果在进程中先关闭了标准输入/输出/错误(0/1/2),在打开一个文件,该文件的fd为(0/1/2)
原理:优先从小到大找没有被占用的文件描述符。
5. 输入重定向
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
umask(0);
close(0);//把标准输入
int fd = open("make_my_file.c", O_WRONLY|O_CREAT|O_TRUNC, 0666); //rw-rw-rw-
if(fd<0)
{
perror("open");//在错误中先打印open 然后出现错误原因
return 1;
}
printf("open success, fd: %d\n", fd);//这里fd为0
char buffer[64];
fgets(buffer,sizeof buffer,stdin);
printf("%s\n",buffer);
close(fd);
return 0;
}
这里本应该从标准输入(键盘)中输入给buffer,这里却从make_my_file.c中输入给了buffer
6. 输出重定向
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
umask(0);
close(1);//把标准输出(输出到显示器) 关闭了
int fd = open("make_my_file.c", O_WRONLY|O_CREAT|O_TRUNC, 0666); //rw-rw-rw-
if(fd<0)
{
perror("open");//在错误中先打印open 然后出现错误原因
return 1;
}
printf("open success, fd: %d\n", fd);//这里fd为1
printf("fd: %d\n", fd);
printf("fd: %d\n", fd);
printf("fd: %d\n", fd);
printf("fd: %d\n", fd);
printf("fd: %d\n", fd);
printf("fd: %d\n", fd);
fprintf(stdout, "hello fprintf\n");
const char *s = "hello fwrite\n";
fwrite(s, strlen(s), 1, stdout);
//已经输出重定向了 往显示屏输入已经变成了往make_my_file文件输入
//如果不刷新缓冲区 close后文件将没有数据
fflush(stdout);
close(fd);
return 0;
}
这些输出原本应该都是往显示器(标准输出)打印的,但是下面的都写入(显示)到了make_my_file.c文件中,这就是输出重定向。
[chy@iZuf6aq431qs6kxxm0vveyZ sometry]$ g++ -o test test.cc
[chy@iZuf6aq431qs6kxxm0vveyZ sometry]$ ./test
[chy@iZuf6aq431qs6kxxm0vveyZ sometry]$ ls
make_my_file.c test test.cc
[chy@iZuf6aq431qs6kxxm0vveyZ sometry]$ cat make_my_file.c
open success, fd: 1
fd: 1
fd: 1
fd: 1
fd: 1
fd: 1
fd: 1
hello fprintf
hello fwrite
7. bash的输出重定向
bash中,需要将脚本demo.sh的标准输出和标准错误输出重定向至文件demo.log
第一种 bash demo.sh 2>demo.log 1>demo.log
第二种 bash demo.sh 1>demo.log 2>&1 先将标准输入重定向为目标文件
再将标准错误输入到已经改变的1
第三种 命令:&> file 将标准输出stdout和标准错误输出stderr重定向至指定的文件file中 bash demo.sh &>demo.log 或者 bash demo.sh >&demo.log
8. 输出重定向的系统调用接口 dup2
int dup2(int oldfd,int newfd);
newfd是oldfd的一个拷贝,输出重定向的时候,我们需要更改文件描述符的指向,让1号描述符指向的位置发生改变,指向我们需要的位置,所以1号描述符是拷贝自我们需要的位置。1对应着newfd
int fd=open("make_my_file,c",O_WRONLY|O_CREAT|O_TRUNC,0666);
dup2(fd,1);
const char* s1="hello";
fprintf(stdin,"%s\n",s1);
把s1输出到make_my_file.c
- 重定向后,oldfd和newfd都会操作newfd所操作的文件
- 重定向前,若newfd已经有打开的文件,则会关闭
8.1 dup2的使用
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main(int argc,char*argv[])
{
if(argc!=2)
{
return 2;
}
int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
if(fd<0)
{
perror("open");
return 1;
}
dup2(fd,1);
fprintf(stdout,"%s/n",argv[1]);
return 0;
}
9. 系统如何管理文件
9.1 如何理解Linux一切皆文件
在冯诺依曼体系中,键盘,显示器,网卡等外设均属于IO设备,也就是说在这些"文件中”一定包含了read和write,尽管对于读写的实现可能完全不相同。但我们依然可以创建对应的struct_file结构体,在结构体内使用函数指针的形式,供我们调用方法。当操作系统需要管理这些外设时,仅需要找到struct_file结构体。而如果这些结构体本来就是用双向链表连接起来的,于是我们看待所有文件的方式都统一成为了struct_file,也就是一切皆文件。
9.2文件如何管理
先描述,后管理
文件分为:磁盘文件(未打开)和内存文件(被进程在内存中打开)。每一个进程都有一个pcb结构体。
在pcb中可以找到一个类型为 struct files_struct * 的指针,(在这里以struct files_struct* files为具体对象)files指向struct files_struct结构体。
在struct files_struct结构体中又可以找到一个struct file* fd_array[ ]的数组指针,其每个成员都是struct file类型,即我们所打开的文件。
以fwrite为例
进程执行fwrite()->需要FILE*指针->FILE封装了fd->write()拿到fd,自己开始执行系统内部的wtite方法->在该进程的pcb结构体中->struct files_struct*->fd_array[fd]->进入struct file 即文件中->进行我们的操作
10. 多进程打开同一文件
原子操作(原子性):不可被中断的一个或一系列操作。
例子:银行转账,A 像 B 转账 100 元。转账这个操作其实包含两个离散的步骤:
- 步骤 1:A 账户减去 100
- 步骤 2:B 账户增加 100
我们要求转账这个操作是原子性的,也就是说步骤 1 和步骤 2 是顺续执行且不可被打断的,要么全部执行成功、要么执行失败。
两个进程打开同一个文件,但是各有各的文件描述信息以及读写位置,互不影响,因此多个进程同时读写有可能会造成穿插覆盖的情况,不具有原子性。
仅读取时,两个不同进程其实都有自己各自的描述信息和读写位置,不受另一进程影响。
若其中一个进程删除了该文件,实际上只是删除文件的目录项,文件的数据以及inode并不会立即被删除,因此若进程已经打开文件,文件被删除时,并不会影响进程的操作,因为进程已经具备文件的描述信息。
若其中一个进程修改了文件,另一个进程可以立即感受到。内容的修改是直接反馈至磁盘文件系统中的,因此当文件内容被修改,其他进程因为也是针对磁盘数据的操作,因此可以立即感知到。