Linux文件IO--文件描述符/重定向
1.基础IO
1.1C语言文件IO
给一个testio.txt写入Hello World:通过操作文件流指针完成。
#include<stdio.h>
#include<string.h>
int main(){
FILE* fp = fopen("testio.txt", "w+");
if(fp == NULL){
printf("打开失败\n");
}
const char* str = "Hello World!\n";
int count = 6;
int len = strlen(str);
while(count--){
fwrite(str, len, 1, fp);
}
fseek(fp, 0 , 0);
char buf[1024] = { 0 };
count = 6;
size_t n;
while(count--){
n = fread(buf, 1, len, fp);
if(n > 0)
printf("%s", buf);
}
if(feof(fp)){
break;
}
}
fclose(fp);
return 0;
}
feof是C语言标准库函数,其原型在stdio.h中,其功能是检测流上的文件结束符,如果文件结束,则返回非0值,否则返回0(即,文件结束:返回非0值;文件未结束:返回0值)。文件结束符只能被clearerr()清除
testio.txt文件成功写入 Hello World
打印 Hello World 到屏幕: 库函数接口完成
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(){
const char* str = "Hello World\n";
int len = strlen(str);
printf(str);
puts("Hello World");
fwrite(str, 1, len, stdout);
fprintf(stdout, str);
fputs(str,stdout);
int i;
for(i = 0; i < len; ++i) {
putchar(*(str + i));
}
for(i = 0; i < len; ++i) {
fputc(*(str +i), stdout);
}
for(i = 0; i < len; ++i) {
putc(*(str + i), stdout);
}
return 0;
}
- printf()函数是格式化输出函数, 一般用于向标准输出设备按规定格式输出
- puts()函数用来向标准输出设备(屏幕)输出字符串并换行,具体为:把字符串输出到标准输出设备,将’\0’转换为回车换行。其调用方式为,puts(s);其中s为字符串字符(字符串数组名或字符串指针)
- fwrite() 是 C 语言标准库中的一个文件处理函数,功能是向指定的文件中写入若干数据块,如成功执行则返回实际写入的数据块数目
- fprintf是C/C++中的一个格式化库函数,位于头文件中,其作用是格式化输出到一个流文件中;函数原型为
int fprintf( FILE *stream, const char *format, [ argument ]...)
- fputs()C语言库函数,把字符串写入到指定的流( stream) 中,但不包括空字符
- putchar原型为
int putchar(int char)
,其功能是把参数 char 指定的字符(一个无符号字符)写入到标准输出 stdout 中,为C库函数 - fputc()函数原型:
int fputc (int c, FILE *fp)
功能: 将字符c写到文件指针fp所指向的文件的当前写指针的位置 - putc()函数原型:
int putc(int char, FILE *stream)
把参数 char 指定的字符(一个无符号字符)写入到指定的流 stream 中,并把位置标识符往前移动。
1.2Linux系统文件IO
Linux还提供了系统调用接口,不调用语言本身的库函数接口,也能实现基础的IO操作:
testio.c:
#include<stdio.h>
#include<string.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main(){
umask(0);
int fd = open("testio.txt", O_RDWR | O_CREAT, 0644);
if(fd < 0){
printf("打开失败\n");
return 0;
}
const char* str = "Hello Linux!\n";
int count = 6;
int len = strlen(str);
while(count--){
write(fd, str, len);
}
lseek(fd, SEEK_SET, 0);
char buf[1024] = { 0 };
count = 6;
size_t n;
while(count--){
n = read(fd, buf, len);
if(n > 0){
printf("%s", buf);
}
else{
break;
}
}
close(fd);
return 0;
}
1.3Linux 系统调用与C库函数对比
打开文件
C语言库函数 | Linux系统调用 | |
---|---|---|
接口名 | fopen() | open() |
原型 | FILE* fopen( const char* filename, const char* mode ) | int open(const char *pathname, int flags) int open(const char *pathname, int flags, mode_t mode) |
参数 | filename: 需要打开的文件名(也可以带路径) mode : 打开方式 "w"(只写), “r”(只读), “a”(追加) " w+"(读写), 若文件已存在, 会清空原文件 “r+”(读写) 若文件已存在, 不会清空原文件 “a+”(读写) 打开一个文件, 在文件末尾进行读写 | filename: 带路径的文件名文件 flags : 文件打开方式 flags选项: 必选项: O_RDRW(读写), O_RDONLY(只读), O_WDONLY(只写) (只能选一个) 常用可选项: O_CREAT(若不存在则创建),O_APPEND(若文件存在, 以追加模式打开),O_TRUNC(若以可写方式打开一个已存在的普通文件, 则将其清空),O_EXCL: 若和O_CREAT同时使用, 若文件不存在则创建, 若已经存在则报错 mode: 用于设置新建文件权限 mode只有当选用O_CREAT时才有效, 否则会被忽略, 此时用于设置新创建文件的预设权限, 预设权限 = mode&(~umask) |
功能/返回值 | 打开指定的文件, 将一个文件流与它关联, 返回这个文件流指针. 若失败返回NULL | 打开指定的文件, 若文件存在, 则返回这个文件的文件描述符fd, 若文件不存在但open创建了则返回新创建的文件的文件描述符fd. 成功时的fd是一个正的小整数. 若失败, 返回值小于 0 |
关闭文件
C语言库函数 | Linux系统调用 | |
---|---|---|
接口名 | fclose() | close() |
原型 | int fclose( FILE* stream ) | int close(int fd) |
参数 | stream : 需要关闭的文件的文件流指针 | fd : 需要关闭的文件的文件描述符 |
功能/返回值 | 关闭与stream这个文件流关联的文件, 并取消文件与stream这个文件流的关联成功返回0, 失败返回EOF(-1) | 关闭一个文件描述符, 使它不再指向任何文件.成功返回0, 失败返回-1 |
写文件
C语言库函数 | Linux系统调用 | |
---|---|---|
接口名 | fwrite() | write() |
原型 | size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream ) | ssize_t write(int fd, const void *buf, size_t count) |
参数 | buffer : 所写数据的地址 size : 数据大小 conut : 数据个数 stream: 文件流指针 | fd: 文件描述符 buf: 要写入文件的数据的地址 count : 需要读取的字节个数 |
功能/返回值 | 从buffer(首地址处)开始向后读取count个size字节数的数据,写入stream所指向文件位置处。返回成功写入的元素总数,如果该数字与count参数不同,则写入错误, 在这种情况下,将为流设置错误指示符(ferror)。成功几个返回几,返回值最小为0 | 向文件描述符fd所引用的文件中写入从buf开始的缓冲区中count字节的数据。POSIX规定,当使用了write()之后再使用 read(),那么读取到的应该是更新后的数据。但请注意并不是所有的文件系统都是POSIX兼容的。ssize_t 可以理解为是有符号的size_t , 即signed size_t调用成功时返回所写入的字节数(若为零则表示没有写入数据)。错误时返回-1,并置errno为相应值。若count为零,对于普通文件无任何影响,但对特殊文件 将产生不可预料的后果 |
读文件
C语言库函数 | Linux系统调用 | |
---|---|---|
接口名 | fread() | read() |
原型 | size_t fread(void *ptr,size_t size,size_t count,FILE *stream) | ssize_t read(int fd, void *buf, size_t count) |
参数 | ptr: 指向大小至少为(size乘count)个字节的内存块的指针,该内存块被转换为void * size: 要读取的每个元素的大小(以字节为单位) count: 元素个数 stream: 指向指定输入流的FILE对象的指针 | fd: 文件描述符 buf: 指向大小至少为count个字节的内存块的指针, 该内存块被转换为void * count : 需要读取的字节个数 |
fseek&lseek
C语言库函数 | Linux系统调用 | |
---|---|---|
接口名 | fseek() | lseek() |
原型 | int fseek ( FILE * stream, long int offset, int origin ) | off_t lseek(int fd, off_t offset, int whence) |
参数 | stream: 文件流指针 offset : 偏移量 origin : 起始位置 | fd: 文件描述符 offset: 偏移量 off_t通常是long类型, 32位平台下为long int, 64位平台下时long long int whence : 起始位置 |
功能/返回值 | 将与流关联的位置指示器设置为新位置,具体为:从起始点origin开始往前或往后偏移offset个字节 | 将与文件描述符fd相关联的打开文件的偏移量重新定位 |
起始点 | 名字 | 用数字代表 |
文件开始位置 | SEEK_SET | 0 |
文件当前位置 | SEEK_CUR | 1 |
文件末尾位置 | SEEK_END | 2 |
fseek(fp,100L,0)(或fseek(fp,100L,SEEK_SET)) 表示将文件位置标记从文件开头往文件末尾方向移动100个字节 fseek(fp,50L,1); (或fseek(fp,50L,SEEK_CUR)) 表示将文件位置标记从当前位置往文件末尾方向移动50个字节 | lseek(fd,100L,0)(或lseek(fd,100L,SEEK_SET)) 表示将文件位置标记从文件开头往文件末尾方向移动100个字节 fseek(fp,50L,1); (或fseek(fp,50L,SEEK_CUR)) 表示将文件位置标记从当前位置往文件末尾方向移动50个字节 |
2.文件描述符
C中每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的,这个结构体类型是在C的标准库中声明的,取名FILE。
struct _iobuf {
char *_ptr;
int _cnt;
char *_base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char *_tmpfname;
};
typedef struct _iobuf FILE;
C中默认会打开三个标准输入输出,分别是标准输入、标准输出、标准错误(三个FILE结构体)。各自都对应一个流指针, 分别是stdin(标准输入流指针)、stdout(标准输出流指针)、strerr(标准错误流指针) ,其类型都是FILE*
。
C的库函数中我们用文件流指针来操作控制文件,但Linux下的系统调用接口用的却是int 型的变量,我们将Linux下的这个整型变量称之为文件描述符(fd)。
2.1基本概念
文件描述符具体就是,内核中的 files_struct* file
这个数组指针所指向的数组的下标(文件描述符是非负数),进程通过这个下标找到具体的struct file,内核利用文件描述符才能访问文件。打开现存文件或新建文件时,内核会返回一个文件描述符,读写文件也需要使用文件描述符来指定待读写的文件。
2.2struct file(file结构体)
struct file描述的是一个打开的文件,系统中每个打开的文件在内核空间都有一个与之关联的 struct file。它由内核在打开文件时创建,并传递给在文件上进行操作的函数。在文件的所有实例都关闭后,内核释放这个数据结构。
struct file {
struct list_head f_list; /*所有打开的文件形成一个链表*/
struct dentry *f_dentry; /*指向相关目录项的指针*/
struct vfsmount *f_vfsmnt; /*指向VFS安装点的指针*/
struct file_operations *f_op; /*指向文件操作表的指针*/
mode_t f_mode; /*文件的打开模式*/
loff_t f_pos; /*文件的当前位置*/
unsigned short f_flags; /*打开文件时所指定的标志*/
unsigned short f_count; /*使用该结构的进程数*/
unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin;
/*预读标志、要预读的最多页面数、上次预读后的文件指针、预读的字节数以及预读的页面数*/
int f_owner; /* 通过信号进行异步I/O数据的传送*/
unsigned int f_uid, f_gid; /*用户的UID和GID*/
int f_error; /*网络写操作的错误码*/
unsigned long f_version; /*版本号*/
void* private_data; /* tty驱动程序所需 */
};
struct file中主要保存了文件位置,还把指向该文件索引节点的指针也放在其中。struct file结构形成一个双链表,称为系统打开文件表,其最大长度是NR_FILE,在fs.h中定义为8192。
struct file总是存在于下面两个双向循环链表的某一个中 :
- “未使用”文件对象的链表 : 该链表既可以用做struct file的内存高速缓存,又可以当作root用户的备用存储器,也就是说即使系统的动态内存用完,也允许超级用户打开文件。由于该链表是未使用的,它们的f_count 为 0,该链表首元素的地址存放在变量free_list中,内核必须确认该链表总是至少包含NR_RESERVED_FILES个对象,通常该值设为10。
- “正在使用”文件对的象链表:该链表中的每个元素至少由一个进程使用,因此,各个元素的f_count不会为0,该链表中第一个元素的地址存放在变量anon_list中。
一个进程打开一个文件或创建一个文件,操作系统在内存中要创建相应的数据结构(struct_file)来描述目标文件,于是就有了file结构体来表示一个已经打开的文件对象。而进程执行open()系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针。所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。
2.3struct files_struct
每个进程用一个files_struct结构来记录文件描述符的使用情况, 这个files_struct结构称为用户打开文件表,它是进程的私有数据,是task_struct中的一员。
struct files_struct {
atomic_t count; /* 共享该表的进程数 */
rwlock_t file_lock; /* 保护以下的所有域,以免在tsk->alloc_lock中的嵌套*/
int max_fds; /*当前文件对象的最大数*/
int max_fdset; /*当前文件描述符的最大数*/
int next_fd; /*已分配的文件描述符加1*/
struct file ** fd; /* 指向文件对象指针数组的指针 */
fd_set *close_on_exec; /*指向执行exec( )时需要关闭的文件描述符*/
fd_set *open_fds; /*指向打开文件描述符的指针*/
fd_set close_on_exec_init;/* 执行exec( )时需要关闭的文件描述符的初值集合*/
fd_set open_fds_init; /*文件描述符的初值集合*/
struct file * fd_array[32];/* 文件对象指针的初始化数组*/
};
fd是用来获取struct file* fd_array[ ]元素的,该数组的长度存放在max_fds中,通常fd_array[ ]包括32个元素。如果进程打开的文件数目多于32,内核就分配一个新的、更大的数组,内核同时也更新max_fds域的值。
对于在fd_array[]数组中的每个元素来说,数组的下标就是文件描述符fd。通常数组的第一个元素(下标为0)是进程的标准输入文件;数组的第二个元素(下标为1)是进程的标准输出文件;数组的第三个元素(下标为2)是进程的标准错误文件。Linux中进程默认有3个缺省打开的文件描述符:标准输入0、标准输出1、标准错误2。
2.4文件流指针与文件描述符
C语言中用FILE结构体来描述管理文件,在Linux下的FILE其实是对文件描述符的进一步封装。每个FILE里都有一个文件描述符,FILE中还新加了一块I/O缓冲区。
为什么要对文件描述符进行封装了 ?
-
不是每一种操作系统管理文件都是用文件描述符来操作的,所以FILE在不同系统下的实现不同,但库函数对外接口与功能要保持一致。FILE就只能在不同系统中实现对不同方式"文件管理方式"的不同的封装。
-
增加IO效率。Linux下的系统调用本身是没有I/O缓冲区的。IO缓冲区可以尽可能减少使用read和write系统调用的次数,从而提高I/O效率。(总共有100个数据,来一个数据读一次和每来十个数据一次读十个,两种方式调用read的次数为100次和10次)
fwrite()的缓冲区:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(){
fwrite("fwrite() ", 1, 10, stdout);
write(1, "write() ", 9);
exit(0);
}
由于fwrite()
缓冲区的存在,“fwrite” 先被写进了缓冲区,在缓冲区没有刷新之前是不会被写入标准输出文件中的,也就是不会打印到屏幕上。直到运行到exit(),进程退出,exit()会刷新缓冲区, 此时才打印"fwrite()" 。
而write()
并不会将数据写入缓冲区,而是会直接写入到标准输出文件中,直接打印到屏幕。所以write() 在前,fwrite()在后。
将exit()改为_exit():
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(){
fwrite("fwrite() ", 1, 10, stdout);
write(1, "write() ", 9);
_exit(0);
}
因为进程退出时,_exit()
不会刷新缓冲区,所以缓冲区中的 "fwrite()"没有被写入到标准输出中,就不会被打印到屏幕。
2.5文件描述符的分配规则
当某个进程打开或创建一个文件时,内核会创建一个struct file,在其files_struct数组当中,找到当前没有被使用的最小的下标,作为新的文件描述符, 并在这个位置存放新的struct file的地址。
文件描述符的分配规则:
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
int main(){
int fd1 = open("file.txt", O_RDONLY);
int fd2 = open("file2.txt", O_RDONLY);
if(fd1 < 0 && fd2 < 0){
perror("open");
return -1;
}
printf("fd1:%d\n", fd1);
printf("fd2:%d\n", fd2);
close(fd1);
close(fd2);
return 0;
}
在Linux下,进程会默认打开标准输入、标准输出、标准错误文件描述,下标为0、1、2位置已经存放这三个文件的struct file的地址。所以在其files_struct数组当中,最小未使用的下标依次为3、4。
关闭默认的文件描述符:
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
int main(){
close(0);
close(2);
int fd1 = open("file.txt", O_RDONLY);
int fd2 = open("file2.txt", O_RDONLY);
if(fd1 < 0 && fd2 < 0){
perror("open");
return -1;
}
printf("fd1:%d\n", fd1);
printf("fd2:%d\n", fd2);
close(fd1);
close(fd2);
return 0;
}
程序一开始就关闭了标准输入(0)和标准错误(2),所以最小未使用两个的下标为刚开始就关闭的0和2。
3.重定向
3.1基本概念
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
int main(){
close(1);
int fd = open("redirect.txt", O_WRONLY | O_CREAT, 0644);
if(fd < 0){
perror("open");
return -1;
}
printf("fd:%d\n", fd);
return 0;
}
当我们执行代码后,生成了redirect.txt,但程序并没有按照我们的意愿将 fd=1 打印到屏幕上,而是将 fd=1 写入了 redirect.txt 中。
原因就是上面说的文件描述符分配规则,有了重定向的概念。
重定向指的是输入输出重定向,输入输出重定向,顾名思义:就是把输入、输出重新指定方向,即不使用linux默认的标准输入、输出设备获取或显示信息,而是指定某个文件做为数据来源或者输出对象。
上例中我们没有使用标准的输出设备,当close(1)关闭了标准输出后, open()将 redirect.txt 替补到了原来标准输出的位置。即没有将 fd=1 打印到屏幕,而是将 redirect.txt 这个文件作为了输出对象,将 fd=1 输出到了 redirect.txt 这个文件中。即将标准输出更改为我们指定某个的文件。
3.2在Shell中的使用
输出重定向
上面的代码就是输出重定向,当close(1)关闭了标准输出后,open()将 redirect.txt 替补到了原来标准输出的位置出,所以本该打印到屏幕的东西却输出到了 redirect.txt 文件中。
追加重定向&清空重定向
在Shell中可以直接用>> 或 >实现输出重定向。
ench “Hello” 将 Hello 打印到屏幕上
使用 >> 输入到 Hello.txt ,如果 Hello.txt 不存在则会尝试创建,并且 >> 是追加输入,每次都是从文件末尾输入。
>
与>>
的区别是 >不会追加,而是直接覆盖输入。
>>
和 >
默认重定向的是标准输出,等效于 1>> 和 1>
错误重定向
在Selle中只需要 2>> 和 2>
输入重定向
<< & <
4. dup2()系统调用
那么Shell中是如何用 << 、< 、>>、> 就实现输入输出的重定向的?实际上Shell先把我们输入的命令,也就是字符串进行拆分,然后根据命令创建子进程,在子进程中调用dup2()来实现的重定向。
dup2():
#include <unistd.h>
int dup2(int oldfd, int newfd)
dup2()使newfd成为oldfd的副本,必要时会先关闭newfd。
注意以下内容:
- 如果oldfd不是有效的文件描述符,则调用失败,并且newfd未关闭
- 如果oldfd是有效的文件描述符,并且newfd的值与oldfd相同,则dup2()不执行任何操作,并且返回newfd
具体使用:open()打开时是追加打开,则dup2()重定向后也是追加重定向,若不是追加打开,则dup2()是清空重定向
清空重定向:
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
int main(){
int fd = open("testdup2.txt", O_CREAT | O_RDWR, 0664);//清空重定向
//int fd = open("testdup2", O_CREAT | O_RDWR | O_APPEND, 0664);//追加重定向
if(fd < 0){
perror("open");
return -1;
}
dup2(fd, 1);
printf("清空重定向\n");
return 0;
}
追加重定向:
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
int main(){
// int fd = open("testdup2.txt", O_CREAT | O_RDWR, 0664);//清空重定向
int fd = open("testdup22.txt", O_CREAT | O_RDWR | O_APPEND, 0664);//追加重定向
if(fd < 0){
perror("open");
return -1;
}
dup2(fd, 1);
printf("追加重定向\n");
return 0;
}