目录
一.C语言文件操作相关库函数的简单使用和操作
二.stdout&&stderr&&stdin
三.系统文件IO
四.文件描述符fd
五.文件描述符的分配规则
五.重定向的原理
在c语言哪里已经结束过c语言相关的文件操作函数的使用。具体请参考博主的文章:
文件操作详解_一个山里的少年的博客-CSDN博客_文件操作。在这里只简单的使用几个与文件相关的库函数fopen.下面请看一段代码:
1 #include
2 int main() 3 { 4 FILE *fp=fopen("log.txt","w");//以写的方式打开这个文件,如果文件不存在会自动创建一个 5 if(fp==NULL) 6 { 7 perror("fopen"); 8 } 9 10 int cnt=5; 11 const char *str="hello Linux\n"; 12 while(cnt--) 13 { 14 fputs(str,fp);//往刚打开的文件中写入数据 15 } 16 17 fclose(fp);//关闭文件 18 19 20 }
在这里我们以写的方式打开一个文件,我们在学习c语言的时候,fopen如果以写的方式打开文件如果这个文件存在那么文件的内容将会被清空,如果不存在会在当前路径下创建这个文件,那什么是当前路径呢?
下面我们将这个程序跑起来.
我们发现确实在当/home/ksy/BK/这个路径下创建了一个log.txt的文件,那么是不是意味着当前路径是可执行程序所在路径了?先不着急我们先使用一个c语言中的读文件的库函数:
1 #include
2 int main() 3 { 4 FILE *fp=fopen("log.txt","r");//以写的方式打开这个文件,如果文件不存在会自动创建一个 5 if(fp==NULL) 6 { 7 perror("fopen"); 8 } 9 //由于上次已经往文件写了,所以我们直接可以读了 10 char buffer[128]; 11 while(fgets(buffer,sizeof(buffer),fp)) 12 { 13 printf("%s",buffer); 14 } 15 return 0; 16 17 } 运行这个程序:
我们发现读取成功了,现在我们来讨论一下所谓的当前路径是不是可执行程序所在的路径了?为了方便测试我们将/home/ksy/BK/下的log.txt删除:使用这一份代码
1 #include
2 int main() 3 { 4 FILE *fp=fopen("log.txt","w");//以写的方式打开这个文件,如果文件不存在会自动创建一个 5 if(fp==NULL) 6 { 7 perror("fopen"); 8 } 9 const char *str="这是一次测试\n"; 10 int cnt=5; 11 while(cnt--) 12 { 13 fputs(str,fp); 14 } 15 return 0; 16 17 } ~ 我们在/home/ksy/BK/生成可执行文件,我们在家目录来运行这个可以执行程序:
我们惊奇的发现竟然在/home/ksy这个路径下生成了一个log.txt,这也充分说明了当前路径不是可执行程序所在路径。而是可执行程序变成进程时,我们在那个目录,就在那个目录创建。
下面我们在稍微演示一下c语言中两个库函数的使用:
下面稍微解释一下这两个函数的使用:
fwrite:第一个参数你要写的内容,第二个参数是指一次写入多少个字节,第三个参数是指最多写几次,第四个参数是指写到那个流里面,返回值是指实际写的次数。下面我们通过一段简单的代码演示一下:
1 #include
2 #include 3 int main() 4 { 5 FILE *fp=fopen("log.txt","w");//以写的方式打开这个文件,如果文件不存在会自动创建一个 E> 6 const char str="hello Linux\n"; 7 int cnt=5; 8 while(cnt--) 9 { 10 fwrite(str,strlen(str),1,fp); 11 } 12 return 0; 13 } 我们将这个程序跑起来:
下面解释一下fread:
fread第一个参数是将读取到的内容放到这里面,第二个参数是指一个读入多少个字节,第三个参数是指最多读几次,第四个参数是指从哪里读取,返回值代码实际读取的次数。
1 #include
2 #include 3 int main() 4 { 5 FILE *fp=fopen("log.txt","r");//以写的方式打开这个文件,如果文件不存在会自动创建一个 6 char buffer[128]; 7 while(fread(buffer,13,1,fp)) 8 { 9 printf("%s",buffer); 10 } 11 return 0; 12 } ~ 运行结果:
我们经常会听说Linux下一切皆文件,也就是说Linux下的任何东西都可以看作是文件,那么键盘和显示器当然也可以看作是文件。我们能看到显示器上的数据,是因为我们向“显示器文件”写入了数据,电脑能获取到我们敲击键盘时对应的字符,是因为电脑从“键盘文件”读取了数据。
当一个c语言程序运行起来时(注意一定是程序运行起来时文件才被打开),会默认打开三个流即stdout(标准输出流),stdin(标准输入流)以及stderr(标准错误流)。其对应的设备分别为:显示器,键盘,显示器。下面我们通过man手册查看一下:
通过man手册我们可以发现他们都是FILE*类型,也就是文件指针。当我们的c程序跑起来时,系统会将这三个输入输出流打开,打开之后我们使用的scanf和printf才能对键盘和显示器进行相关的操作。也就是说,stdin、stdout以及stderr与我们打开某一文件时获取到的文件指针是同一个概念,试想我们使用fputs函数时,将其第二个参数设置为stdout,此时fputs函数会不会之间将数据显示到显示器上呢?下面我们通过一段代码进行验证:
1 #include
2 #include 3 int main() 4 { 5 const char*str="hello ksy\n"; 6 fputs(str,stdout); 7 return 0; 8 } ~ ~ 我们将这个程序跑起来:
我们发现成功的将字符串打印到显示器上面了。当然不是只有c语言有标准输入流,标准输出流,标准错误流,在c++里面也有cin,cout,cerr.其他语言也是具有类似的概念。
操作系统的底层其实给我们提供了文件IO系统调用接口,有的write,read,close和seek登一套系统调用接口,不同的语言会对齐进行封装,封装成对应语言的一套操作文件的库函数,不需要知道底层的调用关系,降低使用者的学习成本。
系统调用接口介绍:open,write,read和close:
1.open
作用:打开一个文件
函数原型如下:
int open(const char*pathname,int flags); int open(const char*pathname,int flags,mode_t mode);
open的第一个参数:pathname
open的第一个参数表示打开或者创建目标文件。在这里要注意的是:
1.如果以路径的形式给出那么当需要创建文件的时候,会在你提供的这个路径下创建。
2.如果只给了文件名,那么会在当前路径下创建(当前路径上面以及提过注意他的含义)。
open的第二个参数:flags
open的第二个参数表示文件打开的方式。常用选项有如下几种:
我们在打开文件时可以使用多个选项中间以|隔开。 举个例子:我们想要以只写的方式打开一个文件如果文件不存在就创建O_WRONLY|O_CREAT。那flags到底是个什么呢?其实它就是一个整数,一个整数有32个比特位,将每一个比特位做为某一个选项,在对应的函数内看那一位是否为1,来判断我们是否传入了这个选项.那么也就意味着O_WRONLY对应的是32个比特位中只有一位是1的整数,到底是不是这样的了?下面我们使用vim打开/usr/include/asm-generic/fcntl.h这个目录下的文件看一看:
我们发现这些宏定义选项的共同点就是,它们的二进制序列当中有且只有一个比特位是1 (O_RDONLY)选项的二进制序列为全0,表示O_RDONLY选项为默认选项)。而在open函数中使用特定的数字进行判断然后只写具体的功能。
open的第三个参数:
第三个参数为设置创建文件的权限,在linux中文件是有权限的。当以只写方式打开文件,文件如果不存在需要创建但是创建时我们需要设置文件的权限,关于权限的问题请参考博主的相关文章。(注意当不创建文件时,第三个参数可以不用填)
open的返回值是指我们打开文件的这个文件描述符打开失败返回-1。 下面我们通过一段代码进行演示:
1 #include
2 #include 3 #include 4 #include 5 #include 6 #include 7 int main() 8 { 9 int fd1=open("./log1.txt",O_WRONLY|O_CREAT,0644); 10 int fd2=open("./log2.txt",O_WRONLY|O_CREAT,0644); 11 int fd3=open("./log3.txt",O_WRONLY|O_CREAT,0644); 12 int fd4=open("./log4.txt",O_WRONLY|O_CREAT,0644); 13 int fd5=open("./log5.txt",O_WRONLY|O_CREAT,0644); 14 int fd6=open("./log6.txt",O_WRONLY|O_CREAT,0644); 15 printf("%d\n",fd1); 16 printf("%d\n",fd2); 17 printf("%d\n",fd3); 18 printf("%d\n",fd4); 19 printf("%d\n",fd5); 20 printf("%d\n",fd6); 21 close(fd1); 22 close(fd2); 23 close(fd3); 24 close(fd4); 25 close(fd5); 26 close(fd6); 27 return 0; 28 } 我们运行这个程序:
我们发现文件描述符是从3开始的并且是连续递增的。如果我们打开一个不存在的文件并且以只读的方式不创建那么此时将会打开失败返回-1.
而所谓的文件描述符本质是一个指针数组的下标,数组中的每个下标都指向了一个存放打开文件信息的结构体,所以我们通过fd(文件描述符)就可以找到对应打开文件的信息。Linux当中会默认打开三个文件,标准输入(0)标准输出(1)标准错误(2)。这也就是为什么我们打开一个文件为什么文件描述符是从3开始的。
2.close
系统中使用close关闭一个文件。对应函数原型
int close(int fd);
关闭文件只需要将对应的文件描述符传入即可,如果关闭文件成功则返回0失败返回-1
3.write
系统接口中使用write函数向文件写入相关信息,write函数的函数原型如下:
第一个参数:对应文件的文件描述符。第二个参数:你要写的内容。第三个参数:你要写多少个字节。返回值:实际写入的字节数。
1 #include
2 #include 3 #include 4 #include 5 #include 6 #include 7 int main() 8 { 9 int fd=open("./log.txt",O_WRONLY|O_CREAT,0644); 10 const char*str="hello word\n"; 11 int cnt=5; 12 while(cnt--) 13 { 14 write(fd,str,strlen(str)); 15 } 16 close(fd); 17 18 return 0; 19 } ~ 我们运行这个程序:
4.read
系统接口中使用read函数从文件读取信息,read函数的函数原型如下:
ssize_t read(int fd, void *buf, size_t count);
第一个参数是文件对应的文件描述符,第二个参数是读取的内容放到这里,第三个参数读取几个字节,返回值为实际读取的字节数读取失败返回-1.
1 #include
2 #include 3 #include 4 #include 5 #include 6 #include 7 int main() 8 { 9 int fd=open("./log.txt",O_RDONLY); 10 char ch; 11 while(1) 12 { 13 ssize_t ret=read(fd,&ch,1); 14 if(ret<=0) 15 { 16 break; 17 } 18 else 19 { 20 write(1,&ch,1); 21 } 22 } 23 24 25 close(fd); 26 27 return 0; 28 } ~
文件是由进程打开,而一个进程是可以打开多个文件。系统中也存在着大量的进程那么也就意味着系统中任何时刻都可能存在大量的进程。而我们打开一个文件,需要将文件的相关属性加载到内存当中,操作系统是做管理工作的软件。那么OS系统需要对应这些数据进行管理如何进行管理?先描述在组织。操作系统会给每个打开的文件创建一个结构体struct_file.并以双项链表的方式将其组织起来。OS的打开文件的管理也就变成了对链表的增删查改等操作。
那么进程怎么知道,那些文件是我打开的了?为了区分文件是那个进程打开的,还需要建立进程和文件之间的对应关系。我们在学习进程的时候,当我们的程序跑起来会将对应的代码和数据加载到内存,并为之创建相关的数据结构(task_struct ,mm_struct,页表)。并通过页表建立虚拟地址和物理地址之间的映射关系。
其实task_struct 有一个指针指向了一下结构体,这个结构体叫做files_struct,结构体里面有一个数组fd_array,而这个数组的下标就是我们说的fd.当进程打开log.txt文件时,我们需要先将该文件从磁盘当中加载到内存,形成对应的struct file,将该struct file连入文件双链表,并将该结构体的首地址填入到fd_array数组当中下标为3的位置,使得fd_array数组中下标为3的指针指向该struct file,最后返回该文件的文件描述符给调用进程即可。
所以我们只要通过文件描述符就可以获取打开文件的相关信息并对其进行一系列操作。我们之前验证了文件描述符默认是从3开始的,也就是说0,1,2是默认被打开的。0代表的是标准输入流,对应硬件设备为键盘;1代表标准输出流,对应硬件设备是显示器;2代表标准错误流,对应硬件设备为显示器。当一个进程被创建时,OS就会根据键盘、显示器、显示器形成各自的struct file,将这3个struct file链接到文件的双链表当中,并将这3个struct file的地址分别填入fd_array数组下标为0、1、2的位置,至此就默认打开了标准输入流、标准输出流和标准错误流。
我们之前连续打开了6个文件,我们发现文件描述符是从3开始的,并且是连续地址的。那真的是一直从3开始吗?下面我们看一段代码:
1 #include
2 #include 3 #include 4 #include 5 #include 6 #include 7 int main() 8 { 9 close(0); 10 int fd1=open("./log1.txt",O_WRONLY|O_CREAT,0644); 11 int fd2=open("./log2.txt",O_WRONLY|O_CREAT,0644); 12 int fd3=open("./log3.txt",O_WRONLY|O_CREAT,0644); 13 int fd4=open("./log4.txt",O_WRONLY|O_CREAT,0644); 14 printf("%d\n",fd1); 15 printf("%d\n",fd2); 16 printf("%d\n",fd3); 17 printf("%d\n",fd4); 18 close(fd1); 19 close(fd2); 20 close(fd3); 21 close(fd4); 22 23 return 0; 24 }
下面我们运行这个程序:
我们发现怎么fd从0开始了,而之后的又是从3开始了。现在我们在将2也关了,我们再来看结果会是如何。
1 #include
2 #include 3 #include 4 #include 5 #include 6 #include 7 int main() 8 { 9 close(0); 10 close(2); 11 int fd1=open("./log1.txt",O_WRONLY|O_CREAT,0644); 12 int fd2=open("./log2.txt",O_WRONLY|O_CREAT,0644); 13 int fd3=open("./log3.txt",O_WRONLY|O_CREAT,0644); 14 int fd4=open("./log4.txt",O_WRONLY|O_CREAT,0644); 15 printf("%d\n",fd1); 16 printf("%d\n",fd2); 17 printf("%d\n",fd3); 18 printf("%d\n",fd4); 19 close(fd1); 20 close(fd2); 21 close(fd3); 22 close(fd4); 23 24 return 0; 25 } ~ ~ 运行结果:
我们发现0和2也被用起来了。现在我们就明白了文件描述符的分配规则是从最小的未被使用的下标开始的
有了上面的基础我们就可以深入的学习我们之前学习过的重定向。理解它的原理是什么.首先我们先来看输入重定项。
1.输入重定项。
我们之前学习过的输出重定向就是,将我们本应该输出到显示器上的数据重定向输出到另一个文件中。那他的原理是什么了?
例如: 如果我们想让本应该输出到“显示器文件”的数据输出到log.txt文件当中,那么我们可以在打开log.txt文件之前将文件描述符为1的文件关闭,也就是将“显示器文件”关闭,这样一来,当我们后续打开log.txt文件时所分配到的文件描述符就是1。
1 #include
2 #include 3 #include 4 #include 5 #include 6 #include 7 int main() 8 { 9 close(1); 10 int fd=open("./log1.txt",O_WRONLY|O_CREAT,0644); 11 printf("hello ksy\n"); 12 printf("hello ksy\n"); 13 printf("hello ksy\n"); 14 printf("hello ksy\n"); 15 close(fd); 16 17 return 0; 18 } ~ 运行结果:
我们发现数据果然打印到了log.txt当中。在这里说明一下:
1.printf默认是向stout打印数据,而stdout也一个FILE*的指针,指向的是一个结果体FILE,这个结构体里面封装了一个整数也就是文件描述符,而stdout指向的就FILE结构体中存放的这个文件描述符就是1,所以printf是向文件描述符为1的文件当中输出数据。
2.c语言中输出数据并不是马上就马上就写入操作系统里面,而是暂时存放在c语言的缓冲区中,当条件到来刷新到缓存区当中。
2.追加重定向
追加重定向和输出重定向的区别是追加重定向不是覆盖数据。
下面我们来看一下原理其实就只比输出重定向多了一个O_APPEND选项
1 #include
2 #include 3 #include 4 #include 5 #include 6 #include 7 int main() 8 { 9 close(1); 10 int fd=open("./log1.txt",O_WRONLY|O_CREAT|O_APPEND,0644);//追加重定向和输出重定向的区别就只是多了一个O_APPEND选项 11 printf("hello ksy\n"); 12 printf("hello ksy\n"); 13 printf("hello ksy\n"); 14 printf("hello ksy\n"); 15 close(fd); 16 17 return 0; 18 }
3.输入重定向
输入重定向就是,将我们本应该从一个键盘上读取数据,现在重定向为从另一个文件读取数据。
比如说我们的scanf函数是从标准输入读取数据,现在我们让它从log1.txt当中读取数据,我们在scanf读取数据之前close(0).这样键盘文件就被关闭,这样一样log1.txt的文件描述符就是0.
1 #include
2 #include 3 #include 4 #include 5 #include 6 #include 7 int main() 8 { 9 close(0); 10 int fd=open("./log1.txt",O_RDONLY);//追加重定向和输出重定向的区别就只是多了一个O_APPEND选项: 11 char buffer[128]; 12 while(~scanf("%s",buffer)) 13 { 14 printf("%s\n",buffer); 15 } 16 close(fd); 17 return 0; 18 } ~ 运行结果:
原理和stdout类似在这里就不说了
思考一个问题:标准输出流和标准错误流对应的都是显示器,它们有什么区别?
下面我们通过一段代码进行验证:
1 #include
2 #include 3 #include 4 #include 5 #include 6 #include 7 int main() 8 { 9 fprintf(stdout,"hello stdout"); 10 fprintf(stderr,"hello stderr"); 11 12 return 0; 13 } 我们发现只有往stdout里面打印的重定项到了log1.txt当中。实际上我们使用重定向时,重定向的是文件描述符是1的标准输出流,而并不会对文件描述符是2的标准错误流进行重定向。这就是两者的区别
系统调用dup2
我们发现我们上面只能通过close关闭对应的文件描述符实习对应的输出重定向和输出重定向,那我们能不能不关闭呢?要完成重定向我们只需对fd_array数组当中元素进行拷贝即可。例如,我们若是将fd_array[3]当中的内容拷贝到fd_array[1]当中,因为C语言当中的stdout就是向文件描述符为1文件输出数据,那么此时我们就将输出重定向到了文件log.txt。而在linux当中就给我们提供了这个系统调用:
函数功能: dup2会将fd_array[oldfd]的内容拷贝到fd_array[newfd]当中。函数返回值:调用成功返回0,失败返回-1
使用的过程中需要注意:
- 如果oldfd不是有效的文件描述符,则dup2调用失败,并且此时文件描述符为newfd的文件没有被关闭。
- 如果oldfd是一个有效的文件描述符,但是newfd和oldfd具有相同的值,则dup2不做任何操作,并返回newfd。
下面通过dup2演示一下前面的输出重定向:
1 #include
2 #include 3 #include 4 #include 5 #include 6 int main() 7 { 8 int fd=open("./log.txt",O_WRONLY|O_CREAT,0644); 9 dup2(fd,1); 10 printf("hello world\n"); 11 printf("hello world\n"); 12 13 } 运行结果:
c语言当中的FILE
因为库函数是对系统调用接口的封装,本质上访问文件都是通过文件描述符fd进行访问的,所以C库当中的FILE结构体内部必定封装了文件描述符fd。我们可以使用vim 打开usr/include/stdio.h的文件查看FILE
完整内容如下:
struct _IO_FILE { int _flags; /* High-order word is _IO_MAGIC; rest is flags. */ #define _IO_file_flags _flags //缓冲区相关 /* The following pointers correspond to the C++ streambuf protocol. */ /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */ char* _IO_read_ptr; /* Current read pointer */ char* _IO_read_end; /* End of get area. */ char* _IO_read_base; /* Start of putback+get area. */ char* _IO_write_base; /* Start of put area. */ char* _IO_write_ptr; /* Current put pointer. */ char* _IO_write_end; /* End of put area. */ char* _IO_buf_base; /* Start of reserve area. */ char* _IO_buf_end; /* End of reserve area. */ /* The following fields are used to support backing up and undo. */ char *_IO_save_base; /* Pointer to start of non-current get area. */ char *_IO_backup_base; /* Pointer to first valid character of backup area */ char *_IO_save_end; /* Pointer to end of non-current get area. */ struct _IO_marker *_markers; struct _IO_FILE *_chain; int _fileno; //封装的文件描述符 #if 0 int _blksize; #else int _flags2; #endif _IO_off_t _old_offset; /* This used to be _offset but it's too small. */ #define __HAVE_COLUMN /* temporary */ /* 1+column number of pbase(); 0 is unknown. */ unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1]; /* char* _save_gptr; char* _save_egptr; */ _IO_lock_t *_lock; #ifdef _IO_USE_OLD_IO_FILE };
从FILE的源码当中我们发现了FILE结构体里面封装了fd,也就是里面的_fileno。不难发现我们在里面还看到了缓冲区。这里的缓冲区指的是c语言当中的缓冲区。缓冲区的刷新策略有如下几种:
1.不缓冲:不经过缓冲区
2.行缓冲:遇到/n就刷新对应打印到显示器的数据采用这种策略
3.全缓冲:缓冲区满了才刷新,或者进程退出时才刷新缓冲区,对于文件采用这种策略
所以我们要明白,重定向会改变缓冲区的刷新策略。比如说输出重定向,将原来的输出到显示器上策略是行缓冲,现在要将其输出到文件当中采用策略的是全缓冲:下面我们来看一个例子:
1 #include
2 #include 3 #include 4 #include 5 #include 6 #include 7 int main() 8 { 9 close(1); 10 int fd=open("./log.txt",O_WRONLY|O_CREAT,0644); 11 if(fd<0) 12 { 13 perror("open"); 14 return -2; 15 } 16 const char*str1="hello write\n"; 17 const char*str2="hello printf\n"; 18 const char*str3="hello fwrite\n"; 19 write(fd,str1,strlen(str1)); W> 20 printf(str2); 21 fwrite(str3,strlen(str3),1,stdout); 22 fork(); 23 fflush(stdout);//刷新 24 close(fd); 25 return 0; 26 } ~ 我们发现为什么只有系统调用fwrite只打印了一次,而printf和fwrite都打印了两次了?这到底是为什么?。这是因为对应系统调用write来说,直接写到OS里面去了,而printf和fwrite会写到了c语言提供的缓冲区里面去了,不会立即刷新到OS里面当fork()之后,子进程和父进程进行写时拷贝,父进程的缓冲区里面的数据会被子进程拷贝一份,父子进程各自刷新各自的。所以我们发现了库函数打印了两次。