基础I/O

库函数I/O接口

  在之前 C 语言进阶中,我们学习了文件的许多操作,包括fopenfclosefreadfwritefseek等等,这部分在这里就不做赘述,如果还有小伙伴不懂得可以先看看我的那篇博客:点此跳转

系统调用I/O接口

open函数
int open(const char* pathname, int flags, mode_t mode);
  • 行为:打开指定文件,返回文件描述符;
  • pathname:要打开文件的路径名;
  • flags:操作标志位,由下面这些选项按位或组成;
    • 以下三个必选其一:
      • O_RDONLY:只读;
      • O_WRONLY:只写;
      • O_RDWR:可读可写;
    • 以下几个为可选项:
      • O_CREAT:若文件不存在则创建新文件;
      • O_EXCL:与选项O_CREAT搭配使用,若文件已经存在则报错,例如大部分 APP 当打开一个时,再次双击打开时会提示已存在;
      • O_TRUNC:打开普通文件时,若该文件已存在且可写,那么会清空文件内容;
      • O_APPEND:向文件输入时总是从文件末尾开始写入;
  • mode:在创建新文件时,所要指定的默认使用权限,因此当打开的文件不存在时,则该参数需要写,否则不用写;mode 以四位数字表示,第一位为 0,后三位为三个八进制数,该权限与通常会被umask修改,所以一般新建文件的权限为 mode & ~umask (也就是 mode 按位与上默认权限掩码取反的值);
    • umask:在 shell 中敲击该命令则会显示默认权限掩码是多少,并且还可以通过该指令修改默认权限掩码,但是如果我们为了创建一个符合自己需求的文件使用权限而将整个 shell 的默认权限掩码都修改了,这样并不合适,所以我们在代码中可以使用umask(四位数字)函数,只对该代码设置特定的权限掩码,从而不影响外面的设置;
  • 返回值:成功则返回一个非负整数,该非负整数称为文件描述符,就相当于文件的操作句柄,失败返回 -1;
write函数
ssize_t write(int fd, const void* buf, size_t count);
  • 行为:通过文件描述符找到打开的文件,再进行内容的写入;
  • fd:通过打开文件所返回的文件描述符;
  • buf:要写入文件的数据所存放的空间地址;
  • count:要写入文件的数据长度,以字节为单位;
  • 返回值:成功写入则返回实际写入的数据长度,以字节为单位,失败则返回 -1;
read函数
ssize_t read(int fd, void* buf, size_t count);
  • 行为:从文件中读取数据放入到指定的内存空间中;
  • fd:通过打开文件所返回的文件描述符;
  • buf:要读出文件的数据所存放的空间地址;
  • count:要读出文件的数据长度,以字节为单位,该长度不能大于 buf 存放空间的长度;
  • 返回值:成功读取则返回实际读取到的数据长度,以字节为单位,失败则返回 -1;
lseek函数
off_t lseek(int fd, off_t offset, int whence);
  • 行为:按照需求跳转打开文件后进行操作的位置;
  • fd:通过打开文件所返回的文件描述符;
  • offset:相对起始参照位置的偏移量,该值如果是负数则代表向前,如果是正数则代表了向后,然后从该位置进行读写操作;
  • whence:起始参照位置,也就是自己规定的起点,SEEK_SET—起始位置,SEEK_CUR—当前位置,SEEK_END—末尾位置;
  • 返回值:成功跳转则返回相对于文件起始位置的偏移量,失败则返回 -1;
close函数
int close(int fd);
  • 行为:关闭通过文件描述符所代表的文件;
  • fd:通过打开文件所返回的文件描述符;
  • 返回值:成功关闭则返回 0,失败则返回 -1;

文件描述符

零碎概念

在这里插入图片描述

  • file结构体:在 Linux 中,当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件,最后组织成了struct file结构体,该结构体我们称之为文件描述信息;
  • files_struct结构体:当在一个进程中执行open系统调用打开一个文件时,必须让进程和文件关联起来,每个进程都有一个指针files_struct*,该指针指向一张表files_struct,表中最重要的部分就是包涵一个指针数组,数组的每个元素都是一个指向打开文件的文件描述信息的指针;
  • 文件描述符fd:是file_struct中指针数组的下标,通过这个下标就能拿到数组中的指针,然后由该指针找到要操作的文件描述信息,进而就可以访问文件;
fd分配规则

  从上面的图片中,我们能大概了解到当进程打开一个文件之后,进程与文件之间的关系主要是通过文件描述符来连接起来的,而我们通过open的返回值也能获取到文件描述符—fd;
  如果我们将 fd 打印出来,那么就会发现,一般情况下该值永远是大于 2 的,并且如果只打开了一个文件的话,那么该值基本是 3,这是因为一个程序在运行之后,就会默认打开三个文件:标准输入、标准输出、标准错误,因此这三个文件分别占用了指针数组的 0、1、2 三个下标,下面进行代码演示:

#include<stdio.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.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;
}

  如果我们在打开文件之前,就先使用close函数关闭了文件描述符 0 号,然后再打开一个文件,那么打开该文件返回的 fd 是等于 0 的,同样的如果关闭了文件描述符 2 号,然后再打开一个文件,那么打开该文件返回的 fd 是等于 2 的,这就说明了,文件描述符的分配规则是:最小未使用数组下标,也就是说,从 0 号开始,哪个未使用就为打开的文件分配那个文件描述符,下面进行代码演示:

#include<stdio.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main(){
	close(2);
	int fd = open("myfile", O_WRONLY|O_CREAT, 00644);
	if(fd < 0){
		perror("open");
		return 1;
	}
	printf("fd: %d\n", fd);
	close(fd);
	return 0;
}

重定向

原理

  通过改变文件描述符所对应的文件描述信息,进而改变了要操作的文件,实现了数据流向的改变;说得通俗一点就是,我们在编写代码时,如果将 1 号文件描述符所指向的文件关闭的话,那么当我们打开一个新文件 test.txt 时,1 号文件描述符所指向的文件将会改变为 test.txt,此时原本将会打印在终端上的内容都会写入到 test.txt 文件中,从而实现了数据输出重定向;
  不过需要注意的是,标准输出 stdout 默认屏幕终端为行缓冲,重定向文件和重定向管道为全缓冲;也就是说如果是正常在终端打印的话,那么打印的内容会在遇到换行或者程序结束而输出到终端上,但是如果是重定向的文件和管道的话,那么内容需要手动刷新缓冲区或者程序结束才写入文件,具体怎么实现实时输出重定向写入,请参考这篇博客:Linux 输出流重定向缓冲设置

另外需要注意的是,如果是代码中实现了重定向写入,那么就要分两种情况(这是我在写代码时发现的,也不知是否正确,但是理论上是这么回事):

  1. 如果是关闭 1 号文件,然后在创建一个新文件使其成为 1 号文件,通过此种方式实现重定向输出,那么就只有手动刷新缓冲区才可写入,否则就算程序结束也无法写入;
  2. 如果是由dup2函数实现的代码中的重定向写入,那么手动刷新缓冲区或者程序结束都可写入文件;

在这里插入图片描述

#include<stdio.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.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);
	//这里属于代码中关闭 1 号文件实现的输出重定向,因此需要加刷新文件缓冲区,否则就算程序结束也无法写入
	fflush(stdout);
	close(fd);
	return 0;
}
dup2函数
int dup2(int oldfd, int newfd);
  • 概念:除了上面的关闭文件外,我们也可以使用特定的函数来实现在代码中输出重定向;
  • 功能:拷贝 oldfd 下标中的指针信息,并将其赋给 newfd 下标中的指针,这样一来看似是向 newfd 中输入,其实是向 oldfd 中输入;
  • newfd:现如今所打开操作的文件的文件描述符;
  • oldfd:需要被重定向文件的文件描述符;
  • 下面写了一个简单的代码,主要功能是读取标准输入的内容,然后再将内容重定向输出到 myfile 文件中;
#include<stdio.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main(){
	int fd = open("myfile", O_CREAT | O_RDWR);
	if (fd < 0) {
		perror("open");
		return 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);
	close(fd);
	return 0;
}

系统调用接口与库函数

概念

  前面我们说过,库函数是在系统调用接口的基础上进行了内容的增加并封装,实现了更多特定功能场景的特殊需求,因此库函数需要实现,最终还是要借助系统调用接口,例如fopen库函数的返回值是 FILE* 指针,而open系统调用接口返回的是整型变量 fd 文件描述符,当使用 FILE*进行操作时,实际上就是在操作 fd;

缓冲区

  库函数在原有基础上增加了一些功能,其中就包括缓冲区;由于 CPU 的处理速率远大于外设的输入输出速率,这样一来输入一个数据或者输出一个数据就进行一次 CPU 与外设的交互,CPU 的效率会大大降低的;于是出现缓冲区来解决这一问题,缓冲区其实就是一块内存,用来存放一定量的数据,等数据存放的差不多时或需要时再进行 CPU 与外设的交互,以便提升 I/O 性能,提升 CPU 运行效率;

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main (){
    printf("printf");
    fprintf(stdout, "fprintf");
    fwrite("fwrite", 6, 1, stdout);
    write(1, "write", 5);
    sleep(3);
    return 0;
}

在这里插入图片描述
  在上面的代码中,需要打印或者写入的字符串都是没有\n换行符的,而从运行结果我们可以看出,write函数在第一时间内就将内容输出,其他几个函数在程序结束之后才将内容输出,这说明了write函数并没有缓冲区,所以内容会在执行的第一时间输出,而其他几个由于没有\n或者刷新缓冲区函数,所以在程序运行结束之后才刷新缓冲区,将内容输出;
  库函数是由缓冲区机制的,而系统调用接口是没有缓冲区机制的,所以在今后的使用中,我们需要根据具体的情况进而做出适当地选择,我么也可以通过手动刷新缓冲区进而实现和库函数类似的功能,一些具体的手动刷新缓冲区的方法大家可以自行上网查阅,或者参考此篇博客:Linux 输出流重定向缓冲设置

shell模拟中加入重定向功能

  在前面的进程操作博客中,当学习到程序替换知识时,我们编写了一个简单的代码——shell 的简单模拟实现,现在,我们了解到重定向概念,因此在我们原先的基础上也增加该功能,使其更加完善;

简单描述一下实现的步骤:
1.捕捉键盘输入,并设置标志位 flag;
2.解析输入内容是否包含重定向字符,遇到 >则替换为 \0,且在此前面的参数为命令参数,在此后面的参数为重定向方式以及重定向文件名程;
3.解析输入信息,得到命令名称以及运行参数;
4.创建子进程;
5.按照 >的个数,选择打开文件的参数选项,对子进程的标准输出进行重定向;
6.在子进程中进行程序替换,如果替换失败则子进程退出;
7.父进程等待子进程退出;

注意:在进程 pcb 中,有个files_sttruct结构体,这是进程打开的文件的各项描述信息,在进行程序替换时,意味着进程会调度一个新的程序运行,初始化虚拟地址空间,进程中原先的代码数据都替换为新的代码和数据了,但是程序替换并不会修改files_struct结构体中的内容,因此程序替换之前打开的文件并不会随着程序替换而关闭,原先重定向信息也不会改变;

#include<string.h>    
#include<sys/wait.h>    
#include<stdlib.h>    
#include<stdio.h>    
#include<unistd.h>    
#include<fcntl.h>    
int main(){    
  //循环进行指令的输入
  while(1){    
  	//设置接收数组
    char buf[1024] = {0};
    //模仿 shell 的格式输出指令提示符    
    printf("[user@bogon ~]$ ");  
    //上一行代码由于换行的话格式就不好看了,所以不能使用 \n,所以可以使用缓冲区刷新函数
    fflush(stdout);
    //获取标准输入中的一行字符
    fgets(buf, 1023, stdin);
    buf[strlen(buf) - 1] = '\0';
    //设置记录>个数的标志位
    int flag = 0;    
    //找到文件名的标志位
    int end = 0;    
    //取文件名
    char* str = NULL;    
    //遍历输入信息,截断命令参数和重定向信息
    for(int i = 0; buf[i] != '\0'; i++){    
      //如果找到>,那么就将其变为\0,并且flag++
      if(buf[i] == '>'){    
        buf[i] = '\0';    
        flag++;    
      }else if(flag != 0 && end == 0 && buf[i] != ' '){    
      	//如果在找到>之后,且第一次遇到字符时,那么该字符串就是文件名,将其提取出来
        str = &buf[i];    
        end++;    
      }else if(flag != 0 && end == 1 && buf[i] == ' ')    
      	//在文件名末尾的后一个位置赋上\0
        buf[i] = '\0';                                                                                                                                                       
    }    
    //设置 esec 替换程序函数所需的参数
    int argc = 0;
    char* argv[32] = {0};    
    char* it = buf;    
    //将从标准输入获取的字符放入到参数数组 argv 中
    while(*it != '\0'){
      //如果遇到的不是空格,那么就将其首地址放入到 argv 中
      if(*it != ' '){
        argv[argc++] = it;    
        //放入后,我们需要将其单个包装起来,也就是将一串没有空格的字符变成字符串,那就是在其后放置 \0
        while(*it != '\0' && *it != ' ')    
          it++;      
        *it = '\0';    
      }    
      //更新变量
      it++;    
    }    
    //exec 函数第二个参数的最后一个是 NULL
    argv[argc] = NULL;    
    //cd 命令是内置指令,因此不能随意使用,所以在遇到该命令之后将操作所在的目录换为需要的即可
    if(strcmp("cd", argv[0]) == 0){    
      chdir(argv[1]);    
      continue;    
    }    
    //创建子进程
    pid_t pid = fork();
    if(pid < 0){
      perror("fork faild");
      continue;
    }else if(pid == 0){
      //在子进程中判断是否需要重定向
      int fd = 1;
      //如果只有一个>,那么就是覆盖重定向
      if(flag == 1)
        fd = open(str, O_RDWR | O_CREAT | O_TRUNC, 0664);
      //如果有两个>,那么就是追加重定向
      else if(flag == 2)
        fd = open(str, O_RDWR | O_CREAT | O_APPEND, 0664);
      //文件打开失败,那么就输出错误信息
      if(fd < 0){
        perror("open error");                                                                                                                                                
        exit(-1);
      }
      //输出重定向
      dup2(fd, 1);
      //程序替换
      execvp(argv[0], argv);
      perror("exec faild");
      exit(-1);
    }
    //父进程等待子进程退出,防止变成僵尸进程
    wait(NULL);
  }                                                                                                                                                                          
  return 0;
}           

静态库与动态库

连接方式
  1. 动态链接:链接动态库生成可执行程序,并没有把库中函数的实现指令直接拿过来写入可执行程序中,而是在可执行程序中记录了库中函数的符号信息表,在执行程序的时候需要去加载动态库到内存中;
    • 优点:程序小,运行时动态库被加载到内存中;可共享,可以多个程序使用同一份内存中库函数的代码,减小冗余;便于模块代码替换;
    • 适用:一些模块化、便于功能替换的接口适用动态库;
    • 缺点:依赖强,运行需要依赖动态库,若动态库不存在,则无法运行;
  2. 静态链接:链接静态库生成可执行程序,直接将库中我们用到的函数的实现代码指令,写入到了可执行程序文件中;
    • 优点:无依赖,不需要任何外物就可直接运行;
    • 适用:功能改动小,并且只有当前程序使用的时候使用静态库;
    • 缺点:程序大,可执行程序中会加载很多函数;冗余高,若多个程序使用同一个库函数,则这个库函数会被包含多份;
生成方式
静态库
  1. 将每个原码文件,先编译汇编成为机器指令代码;
    • gcc -c xxx.c -o xxx.o:先将.c的程序代码使用 gcc 编译器生成为.o链接文件;
  2. 将用到的机器指令代码打包到一起;
    • ar -cr lib库名称.a xxx.o ...:再使用该命令将多个链接文件生成为静态库文件,静态库文件的名字是固定格式的,以 lib 开头,再以 .a 结尾;
  • ar -tv lib库名称.a:查看静态库中的目录列表,-t—列出静态库中的文件,-v—显示详细信息;
动态库
  1. 将每个原码文件,先编译汇编成为机器指令代码;
    • gcc -fPRC -c xxx.c -o xxx.o:先将.c的程序代码使用 gcc 编译器生成为.o链接文件,加上-fPRC选项是为了生成位置无关代码;
  2. 将用到的机器指令代码打包到一起;
    • gcc --shared xxx.o ... -o lib库名称.so:再使用该命令将多个链接文件生成共享库格式,动态库文件的名字是固定格式的,以 lib 开头,再以 .so 结尾;
使用方式
生成可执行程序时连接使用
  • gcc xxx.c -o xxx.exe -l库名称:在将程序代码使用 gcc 编译器生成可执行程序时链接库文件,连接方式为-l选项后直接接库文件名称,此处库文件名称为去掉前后缀之后的那部分;
  • 在使用该方式之前,需要注意 gcc 编译器会到指定路径下去找对应的库文件,也就是说使用某个库文件,那么在指定的路径下应该包含该库文件,一般默认路径为——/usr/lib64,那么完成这一目的的方法有以下三种:
    1. 将库文件放到指定路径下:cp libxxx.a /usr/lib64
    2. 设置环境变量LIBRARY_PATH:export LIBRARY_PATH=$LIBRARY_PATH:库所在路径;
    3. 使用 gcc 的-L选项指定库文件所在路径:gcc xxx.c -o xxx.exe -L库文件所在路径 -l库文件名称
运行可执行程序时加载使用(仅针对动态链接的动态库)
  • 生成可执行程序时连接的是动态库,那么在运行时会出现问题,要想解决就需要在运行前使用下面两种方法:
    1. 库文件需要放到指定的路径下:cp libxxx.so /usr/lib64
    2. 设置环境变量LD_LIBRARY_PATHexport LD_LIBRARY_PATH=$LD_LIBRARY_PATH:库所在路径

文件系统

存储方式

在这里插入图片描述
  在 Linux ext2 文件系统中,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,它分区被划分为一个个的 block,一个 block 的大小是由格式化的时候确定的,并且不可以更改,其中各项信息如下:

  • Block Group:ext2 文件系统会根据分区的大小划分为数个 Block Group,而每个 Block Group 都有着相同的结构组成,被分为多个区块,每个区块中记录着不同的信息,下面为各区块含义;
  • Super Block(超级快):存放文件系统本身的结构信息,记录的信息主要有:bolck 和 inode 的总量、未使用的 block 和 inode 的数量、一个 block 和 inode 的大小、最近一次挂载的时间、最近一次写入数据的时间、最近一次检验磁盘的时间等其他文件系统的相关信息,Super Block 的信息被破坏,可以说整个文件系统结构就被破坏了;
  • Group Descriptor Table(块组描述符):描述块组属性信息,有兴趣的小伙伴可以自己上网解一下;
  • Block Bitmap(块位图):Block Bitmap 中记录着 Data Block 中哪个数据块已经被占用,哪个数据块没有被占用;
  • inode Bitmap(inode 位图):每个 bit 表示一个 inode 是否空闲可用;
  • inode table(inode 节点表):存放文件属性如:文件大小、所有者、权限、最近修改时间、在磁盘哪些区块存放等等;
  • date blocks(数据区):存放文件数据内容;
inode概念

  在系统中文件存储在磁盘区块,而关于文件的具体信息是记录在 inode 节点表中的,每一个 inode 节点都记录了文件的:大小、权限、时间…所存储的磁盘区块号等等;
  每一个 inode 节点都对应了一个 inode_id,通过 inode_id 我么就可以拿到一个 inode 节点信息,而每一个文件在目录下的存放包括两个信息:inode_id + 文件名称,通常我们使用ls命令只能查看到文件名称,但是我们可以通过以下方式查看到每一个文件对应的 inode_id:

  • ls -i:查看当前目录下的文件,并且显示它们的 inode_id;
  • ls -i 文件名:查看当前目录下指定文件的 inode_id;
  • stat 文件名:查看指定文件的详细信息,其中一项就包括了 inode_id;
文件创建
  1. 存储属性
    内核先找到一个空闲的 inode 节点,这里假设找到的节点的 inode_id 为 263466,内核把文件信息记录到其中;
  2. 存储数据
    假设某个文件需要存储在三个磁盘块,内核找到了三个空闲块:300、500、800,将内核缓冲区的第一块数据复制到 300,下一块复制到 500,以此类推;
  3. 记录分配情况
    文件内容按顺序 300,500,800 存放,内核在inode上的磁盘分布区记录了上述块列表;
  4. 添加文件名到目录
    假设新的文件名为 abc.txt,内核将入口(也就是inode_id + 文件名称:263466,abc.txt)添加到目录文件,文件名和 inode_id 之间的对应关系将文件名和文件的内容及属性连接起来;
建立链接
硬链接

  通过上面的知识,我们可以了解到,真正找到磁盘上文件的并不是通过文件名,而是通过 inode,其实在 linux 中可以让多个文件名对应于同一个 inode,而这种方式我们称之为两个文件间的硬链接,可以通过下面的命令实现:

  • ln 源文件名称 新创建文件名称:创建一个新文件,使得新文件与源文件(已存在文件)之间建立硬链接关系;
      建立链接之后,源文件与新文件的链接状态完全相同,他们被称为指向同一份文件的硬链接,内核记录了这个连接数为 2。
      我们在删除文件时干了两件事情:1.在目录中将对应的记录删除,2.将硬连接数 -1,如果为 0,则将文件存放的对应的磁盘释放;
软链接

  硬链接是通过同一个 inode 引用不同的文件来实现,软链接则是通过不同的 inode 来存放源文件的访问路径来实现,因此源文件被删除,那么软链接就失效了,软链接可以通过以下命令来实现:

  • ln -s 源文件名称 新创建文件名称:创建一个新文件,使得新文件与源文件之间建立软链接关系;
    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值