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_SET0
文件当前位置SEEK_CUR1
文件末尾位置SEEK_END2
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缓冲区。

为什么要对文件描述符进行封装了 ?

  1. 不是每一种操作系统管理文件都是用文件描述符来操作的,所以FILE在不同系统下的实现不同,但库函数对外接口与功能要保持一致。FILE就只能在不同系统中实现对不同方式"文件管理方式"的不同的封装。

  2. 增加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;
}                                                                           

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值