文件
文件=文件内容+文件属性(文件属性也是数据,即使创建一个空文件,也要占据磁盘空间)
文件操作 = 文件内容的操作+文件属性的操作(在操作文件时可能即改变了文件内容又改变文件属性)
所谓打开文件,究竟是干什么?
打开文件是手段,目的是对文件进行操作,打开文件,就是将文件加载到内存,这是由冯诺依曼体系结构决定的(CPU只能对内存进行读写,不能访问其他外设,所以对文件操作,需要先将文件加载到内存)
当文件没有处于打开状态时文件储存在磁盘上
打开的文件(内存文件)和磁盘文件
当我们写的文件操作代码——>程序,程序运行的时候,执行相应的代码,才会真正的对文件进行相关操作
进程和打开文件
C语言文件操作
FILE* fp=fopen("log.txt","w");//以写的方式打开文件
if(fp==NULL)
{
perror("fopen");
return 1;
}
const char* msg="hello 104";
int cnt=1;
while(cnt<20)
{
fprintf (fp,"%s:%d",msg,cnt++);//将%s:%d写入文件
}
fclose(fp);//关闭文件
- 默认这个文件在当前路径生成
当前路径:当前进程的工作路径
工作路径:进程在哪个路径运行,这个路径就是这个进程的工作路径
可以使用系统调用接口
chdir()
改变进程的当前路径
- 写入的方式打开文件(如果)
fopen("log.txt","w"); //用写的方式打开文件,会先将文件清空 fopen("log.txt","a"); //用写的方式打开文件,以追加写入
写入文件到底是怎么写入的?
写入文件是向磁盘中写入,磁盘是硬件,所以对硬件写入都是通过操作系统提供的系统调用接口完成的,我们C语言中使用的函数是对系统接口做了封装。
进程和打开文件的关系(内存级)
文件系统级别的接口
open
int open(const char*pathname,int flags); int open(const char*pathname,int flags,mode_t mode);
返回值:成功返回文件描述符,失败返回-1
参数:
patnname
:打开的文件名
flags:O_RDONLY(只读),O_WRONLY(只写),O_RDWR(可读可写),O_APPEND(追加写入),O_CREAT(不存在创建),O_TRUNC(写入时先清空)
(宏),系统传递标记位,用位图结构来进行传递的,每一个宏标记,一般只需要一个比特位是1,并且和其他宏对应的值,不能重叠比如:
O_RDONLY
0000 0001,O_WRONLY
0000 0010
mode
:创建时文件的文件的读写权限(mask)//创建后真正的权限是mask&!unask
int main() { int fd = open("test.txt",O_WRONLY|O_CREAT,0666); //不会清空,直接覆盖写入 //int fd=open("test.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);//会清空后,再写入 //int fd=open("test.txt",O_WRONLY|O_CREAT|O_APPEND,0666);//追加写入 char buf[]="qwer"; // write(fd,buf,sizeof(buf)); //写入文件 read(fd,buf,4); //从文件读取4个字节到buf printf("%s\n",buf); close(fd); return 0; }
补充:命令行清空文件
> test.txt
write
write(fd,buffer,sizeof(buffer)-1); //-1是因为把数据写入到文件里,文件不认识'\0',所以在文件中会出现乱码(编译器认识'\0')
read
read(fd,buffer,sizeof(buffer)-1);
文件描述符
int main() { int fd1=open("test1.txt",O_WRONLY|O_CREAT|O_TRUNC,0666); printf("fd1:%d\n",fd1); int fd2=open("test2.txt",O_WRONLY|O_CREAT|O_TRUNC,0666); int fd3=open("test3.txt",O_WRONLY|O_CREAT|O_TRUNC,0666); printf("fd2:%d\n",fd2); printf("fd3:%d\n",fd3); close(fd1); close(fd2); close(fd3); return 0; }
从上述代码可以看出文件描述符从3开始
那文件描述符的这个数字到底是什么以及为什从3开始,0,1,2呢?
我们首先了解文件的原理
一个进程可能会打开多个文件,打开的文件会被加载到内存中,那么如何管理这些文件呢?
管理文件就是将文件先描述,在组织
一个文件被打开,在内存中就会创建文件的数据结构(结构体),再通过链表的方式链接起来
struct file { //包含了文件的大部分内容+属性 struct file* next; struct file* prev; }
一个进程可能会打开多个文件,进程的
pcb(task_struct)
中会有一个结构体指针,指向一个结构体,这个结构会有一个结构体指针数组成员,这个数组中的结构体指针就指向描述文件的数据结构,这么一个数组下标就对应指向结构体对应文件的描述符。从3开始是因为,0,1,2一开始就打开了,0,1,2——>
stdin(标准输入,键盘),stdout(标准输出,显示器),stdree(标准错误,显示器)
补充:C语言中的FILE*是一个结构体指针,因为C语言文件函数是封装系统接口,那么C语言中的FILE中一定封装了
fd
FILE* f=fopen("bite","r"); printf("%d",f->_fileno); //fd
文件描述符的分配规则
从头遍历这个结构体指针数组,找到一个最小的且没有被使用的下标,分配给新的文件
重定向的本质
把1对应的文件关掉,打开一个文件
本来向显示器打印,最后向一个文件打印(重定向)
stdout
标准输出,只往1指向的文件输出,不关心1到底指向谁文件操作只认
fd
,不关心fd
到底指向那个文件如果我们将来要进行重定向,上层只认0,1,2,3,4这样的
fd
,我们可以在os
内部,通过一定的方式调整数组特定下标的内容(指向),我们就可以完成重定向操作
具体操作
上面的一堆数据,都是
os
内核,只能使用os
提供的系统调用系统调用
int dup2(int oldfd,int newfd);
是把
fd_array
数组上的内容拷贝,而不是拷贝数组下标把数组
oldfd
下标得内容拷贝到数组下标newfd
内比如:
dup2(fd,1); //把fd内容拷贝到1中 //这样在fd_array[]数组中下标1中的内容和下标fd的内容一样,这时,我们向标准输出打印,就不会像显示器打印,而是向文件中打印
返回值:
失败返回-1,成功返回一个文件描述符
理解缓冲区
缓冲区的本质
就是一段内存
缓冲区在哪里?
先看一段代码
int main() { printf("hello write1"); fputs("hello write2",stdout); fprintf(stdout,"hello write3"); write(1,"hello write4",12); sleep(5); return 0; }
可以看出,
"hello write4"
马上刷新出来,而其余的是sleep5秒之后,进程退出,刷新缓冲区,才输出的。由此可得,系统并没有为我们提供缓冲区,而C语言的函数,却有缓冲区,说明缓冲区是由语言提供的
C语言接口封装了write接口,C语言接口是数据是先放进缓冲区的,等到要刷新缓冲区时,再调用write函数
那么缓冲区到底在那?
缓冲区是语言提供的,这几个C语言接口有一个共同的参数:
stdout
,stdout
是FILE*类型,FILE是C语言的文件类型,它是一个结构体,结构体中除了包含fd
(文件描述符),还有一部分就是缓冲区。那么我们的代码在执行时,会把数据先写入到FILE中的的缓冲区中,等待缓冲区积累到一定量时,在把缓冲区中的数据通过
write(fd,……)
刷新到内核空间中(因为打开文件,文件就会加载到内核空间中),再通过synfcs
系统接口,把数据再刷新到硬件(这才真正把数据写入到磁盘中)1
在刚刚说过,缓冲区在积累一定量时刷新,刷新策略是什么?
- 常规
无缓冲(立即刷新)
行缓冲(逐行缓冲)显示器文件
全缓冲(缓冲区满,刷新)磁盘文件
- 特殊
进程退出
用户强制刷新(
fflush
)
标准错误
标准错误的输出和标准输出的输出都为显示器,但是标准输出和标准错误的文件描述符不同,所以将标准输出重定向后,标准错误的输出依旧在显示器上
int main() { //fopen: C库函数 int fd = open("log.txt", O_RDONLY);//必定失败的 //stdout printf("hello printf 1\n"); fprintf(stdout, "hello fprintf 1\n"); fputs("hello fputs 1\n", stdout); //stderr fprintf(stderr, "hello fprintf 2\n"); fputs("hello fputs 2\n", stderr); perror("hello perror 2"); //cout std::cout << "hello cout 1" << std::endl; //cerr std::cerr << "hello cerr 2" << std::endl; return 0; }
重定向标准输出
重定向标准输出,标准错误到两个文件
重定向标准输出,标准错误到一个文件
./test > all.txt 2>&1 //先标准输出重定向,再把fd_array[]下标1中的内容拷贝到下标2中 //重定向 > 前不加数字,默认标准输出重定向