Linux-基础IO(续)

 2、fd分配规则

  • 文件描述符分配规则:

在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符

  • 示例1:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
    int fd = open("myfile", O_RDONLY);
    if(fd < 0){
        perror("open");
        return 1;
    }
    printf("fd: %d\n", fd);
    close(fd);
    return 0;
}

结果:输出3

  • 示例2:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
    close(0);
    //close(2);
    int fd = open("myfile", O_RDONLY);
    if(fd < 0){
        perror("open");
        return 1;
    }
    printf("fd: %d\n", fd);
    close(fd);
    return 0;
}

结果:关闭0输出0,关闭2输出2

那我们就可以发现文件描述符的分配规则就是:
在file_struct数组中,找到当前没有被使用的最小的下标,作为新的文件描述符。
下图就是close(0),后,再使用open打开一个文件时文件描述符的分配规则

四、重定向

1、概念及演示

Linux 中标准的输入设备默认指的是键盘,标准的输出设备默认指的是显示器

  • 输入/输出重定向:
  1. 输入重定向:指的是重新指定设备来代替键盘作为新的输入设备

  2. 输出重定向:指的是重新指定设备来代替显示器作为新的输出设备

注:通常是用文件或命令的执行结果来代替键盘作为新的输入设备,而新的输出设备通常指的就是文件

  • 常用重定向:
命令符号格式作用
命令 > 文件命令的结果输出到文件上
命令 >> 文件命令的结果追加输出到文件上
命令 < 文件 1 > 文件 2将文件 1 作为命令的输入设备,该命令的执行结果输出到文件 2 中
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
    close(1);//关闭标准输出
    int fd = open("myfile", O_WRONLY|O_CREAT, 00644);
    if(fd < 0){
        perror("open");
        return 1;
    }
    printf("fd: %d\n", fd);
    fflush(stdout);//强制输出
    close(fd);
    exit(0);
}

注:本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中fd=1,这种现象叫做输出重定向。因为把标准输出关闭了,所以,open分配的fd就成了1;因此,输出重定向到了myfile上。

  • 重定向本质:

从上述示例来看,输出重定向是将进程中的文件指针数组中的标准输出stdout文件给关闭(并非真正关闭,而是将指针数组对应下标的内容置空),再将新打开文件分配到标准输出文件对应的下标上,再输出时,系统不知道文件已经替换,依旧输出到stdout文件对应的数组下标为1的文件上,但是此时文件已经被替换了

2、dup2系统调用

  • 函数原型:
#include <unistd.h>
int dup2(int oldfd, int newfd);//将文件地址oldfd替换到newfd上
  • 示例:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
    int fd = open("log.txt", O_CREAT | O_RDWR);
    if (fd < 0) {
        perror("open");
        return 1;
    }
    close(1);
    dup2(fd, 1);//输出重定向
    char buf[1024] = {0};
    ssize_t read_size = read(0, buf, sizeof(buf) - 1);
    if (read_size < 0) {
        perror("read");
    }
    printf("%s", buf);
    fflush(stdout);
    return 0;
}

3、重定向的原理

注:重定向与程序替换是可以同时进行,重定向改变的是进程PCB中的文件指针数组中的文件地址信息,而程序替换则是触发写时拷贝将进程地址空间的代码和数据进行替换,这之间没有影响

  • 输出重定向示例:命令 cat test.c > myfile

系统创建子进程exec替换程序执行cat test.c命令之前,先将标准输出文件关闭,并打开myfile文件(如果不存在则创建,对应的open选项则是O_WRONLY|O_CREAT)

  • 追加重定向示例:命令 cat test.c >> myfile

这里大致和输出重定向一样,只不过open的选项改为O_APPEND|O_CREAT

  • 输入重定向示例:命令 mycmd > test.c

系统创建子进程exec替换程序执行 test.c 命令之前,先将标准输入文件关闭,并打开 mycmd 文件(对应的open选项则是O_RDONLY)

4、缓冲区和刷新策略

  • 示例:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
    close(1);//关闭标准输出
    int fd = open("myfile", O_WRONLY|O_CREAT, 00644);
    if(fd < 0){
        perror("open");
        return 1;
    }
    printf("fd: %d\n", fd);
    //fflush(stdout);
    close(fd);
    exit(0);
}
  • 解释:
  1. 这里明明将输出结果重定向到文件myfile中,但是myfile文件并没有内容,与上面示例的区别是在文件关闭之前并没有将结果给强制刷新
  2. 对于文件结构体来说,里面除了读写方法外,还存在着缓冲区,在正式刷新到磁盘上对应的文件之前,数据先是由文件缓冲区保存着
  3. 对于标准输出的刷新策略是行缓冲,当遇到\n时触发刷新机制,对于普通文件来说则是全缓冲,当缓冲满时就进行刷新,而强制刷新以及进程结束刷新对两者都有效
  4. 这里输出重定向之后指针数组对应的原标准输出文件的替换成了普通文件,数据写到对应文件缓冲区里,同时对应刷新策略也改变成全缓冲,关闭文件之前没有强制刷新,则数据也就没写到对应磁盘上的文件里
  • 刷新策略:
  1. 无缓冲:无缓冲的意思是说,直接对数据进行操作,无需经过数据缓冲区缓存,系统调用接口采用此方式

  2. 行缓存:缓冲区的数据每满一行即对数据进行操作,而通常情况下向屏幕打印数据就是行缓存方式

  3. 全缓冲:缓冲区的数据满时才对数据进行操作,通常向文件中写数据使用的就是全缓冲方式

五、文件及文件系统

1、FILE

  • 概念:

因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的,所以C库当中的FILE结构体内部,必定封装了fd

  • 示例:
#include <stdio.h>
#include <string.h>
int main()
{
    const char *msg0="hello printf\n";
    const char *msg1="hello fwrite\n";
    const char *msg2="hello write\n";
    printf("%s", msg0);
    fwrite(msg1, strlen(msg0), 1, stdout);
    write(1, msg2, strlen(msg2));
    fork();
    return 0;
}
  • 运行出结果:
hello printf
hello fwrite
hello write  
  • 输出重定向结果: ./hello > file
hello write
hello printf
hello fwrite
hello printf
hello fwrite

区别:这里 printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用),而这与就和fork有关

  • 解释:
  1. printf fwrite 库函数是C语言上的函数,这些库函数在实现输出时必定通过调用C语言的文件IO函数实现。C语言文件IO函数的返回类型是FILE*,这里的FILE是C语言上的文件结构体,其中为了实现语言与系统层面的相连,FILE结构体里也存在着_fileno(对应fd)以及用户层面的缓冲区,所以库函数输出数据是先输出到FILE文件结构体里的缓冲区
  2. 如果是直接运行,即没有发生输出重定向时,向显示屏文件的刷新机制是行缓冲(遇到\n则刷新),即立即将缓冲数据给刷新,fork之后没有什么作用
  3. 当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲(普通文件是全缓冲的,缓冲满则刷新),即FILE中缓冲区存有数据,当fork之后,子进程会与父进程代码共享,数据各有一份(刷新就是写入,发生写时拷贝),程序结束退出时强制刷新数据,所以库函数调用的都输出了两次
  4. write 为系统接口无缓冲机制,就直接将数据刷新

  • 注意:
  1. OS内核区实际上也是有缓冲区的,当我们刷新用户缓冲区的数据时,并不是直接将用户缓冲区的数据刷新到磁盘或是显示器上,而是先将数据刷新到操作系统缓冲区,然后再由操作系统将数据刷新到磁盘或是显示器上
  2. 操作系统也有自己的刷新机制,这样的分用户层面和内核层面的缓冲区,便于用户层面与内核层面进行解耦
  • FILE结构体:
//在/usr/include/stdio.h
typedef struct _IO_FILE FILE; 
//在/usr/include/libio.h
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 di rectly. */
    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
};

2、文件系统

  • 命令 ls -l 查看文件信息:
[root@localhost linux]# ls -l
总用量 12
-rwxr-xr-x. 1 root root 7438 "9月 13 14:56" a.out
-rw-r--r--. 1 root root 654 "9月 13 14:56" test.c

每行包含7列:模式;硬链接数;文件所有者;组;大小;最后修改时间 ;文件名

  • 命令 stat 查看文件信息:
[root@localhost linux]# stat test.c
File: "test.c"
Size: 654 Blocks: 8 IO Block: 4096 普通文件
Device: 802h/2050d Inode: 263715 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2017-09-13 14:56:57.059012947 +0800
Modify: 2017-09-13 14:56:40.067012944 +0800
Change: 2017-09-13 14:56:40.069012948 +0800
  • 注意:
  1. Access最近访问文件时间(不会立即刷新,访问是一个比较频繁的行为,立即刷新则会减缓效率)

  2. Modify:最近修改文件的时间(主要是文件的内容,立即更新)

  3. Change:最近修改文件属性的时间(修改文件内容可能会造成文件属性的修改,立即更新)

  • 如何读取文件信息:

通过读取存储在磁盘上的文件信息,然后显示出来

  • 确定磁盘的读写文件:

确定读写信息在磁盘的哪个盘面/柱面/扇区,但是这样的方式并不便于移植,由此我们将磁盘抽象成数组,数组的下标是单调递增不重复的数字,可以直接确定要读写的文件

  • 分区管理:

磁盘分区就是使用分区编辑器在磁盘上划分几个逻辑部分,盘片一旦划分成数个分区,不同的目录与文件就可以存储进不同的分区,分区越多,就可以将文件的性质区分得越细,按照更为细分的性质,存储在不同的地方以管理文件

磁盘是典型的块设备,硬盘分区被划分为一个个block,一个block的大小是由格式化的时候确定的,并且不可以更改

  • 如何进行管理:

  • 示图:

  • 说明:
  1. Boot Block:该区域磁盘文件的驱动文件,如果驱动损坏,那么则无法进行读取对应区域的文件信息及数据
  2. Block Group:ext2文件系统会根据分区的大小划分为数个Block Group,而每个Block Group都有着相同的结构组成
  3. Super Block:存放文件系统本身的结构信息,记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了
  4. Group Descriptor Table:块组描述符,描述块组属性信息,整体group的空间使用信息,以及其他信息
  5. Block Bitmap:Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用
  6. inode Bitmap: inode位图当中记录着每个inode是否空闲可用
  7. inode Table:存放文件属性,即每个文件的inode,每个文件对应一个inode,而inode才是标识文件的唯一方式
  8. Data Blocks:存放inode对应的文件数据

注:其他块组当中可能会存在冗余的Super Block,当某一Super Block被破坏后可以通过其他Super Block进行恢复;磁盘分区并格式化后,每个分区的inode个数就确定了

  • 如何理解创建一个文件:

通过遍历inode位图的方式,找到一个空闲的inode,在inode表当中找到对应的inode,并将文件的属性信息填充进inode结构中,并将该文件的文件名和inode的映射关系添加到目录文件的数据块中,如果写入内容,需要通过Block Bitmap找到闲置的数据块,将数据写入数据块,并将映射关系写到inode结构中

  • 如何理解对文件写入信息:

通过目录文件中的数据块找到文件名及其inode的映射,再找到对应的inode结构,再通过inode结构找到存储该文件内容的数据块,并将数据写入数据块;若不存在数据块或申请的数据块已被写满,则通过遍历块位图的方式找到一个空闲的块号,并在数据区当中找到对应的空闲块,再将数据写入数据块,最后还需要建立数据块和inode结构的对应关系

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值