Linux 基础 I/O

目录

1. 标准库的IO接口 

2. 系统调用的IO接口 

3. 文件描述符

3.1 什么是文件描述符

3.2 文件描述符的分配规则

3.3 文件流指针与文件描述符的关系

3.5 缓冲区位置

3.6 通过文件流指针操作一个文件

3.7 内核态与用户态

4. 重定向

4.1 dup2

4.2 在minishell中添加重定向

5. 文件系统(ext2)

5.1 文件存储与读取

5.2 软链接与硬链接

6. 动态库与静态库

6.1 动态库的生成

6.2 静态库的生成

6.3 库的使用


1. 标准库的IO接口 

  • fopen:打开文件
    • fopen(file,参数)
      • 参数
        • r:若文件存在,以只读打开;不存在,报错
        • r+:若文件存在,以读写打开;不存在,报错
        • w:若文件不存在则创建;存在,清空原有内容,以只写打开
        • w+:若文件不存在则创建;存在,清空原有内容,以读写打开
        • a:若文件不存在则创建;存在,以追加写方式打开
        • a+:若文件不存在则创建;存在,以读和追加写方式打开
  • fseek:跳转读写位置
    • int fseek(FILE *stream,long offset,int whence);
      • 对文件读写位置从whence开始偏移offset个字节
      • whence的参数
        • SEEK_SET:从文件起始位置开始偏移
        • SEEK_CUR:从当前读写位置开始偏移
        • SEEK_END:从文件末尾位置开始偏移
      • 返回值
        • 从文件起始位置到当前跳转位置的偏移量
  • fwrite:写按块入数据
    • size_t fwrite(void *ptr,size_t size,size_t nmemb,FILE *stream) ;
      • 写数据时,可以分块写入。
      • size:块大小 nmemeb:块数量  两者相乘=字节大小
      • 返回:写入块个数
  • fread:读出数据
  • fclose:关闭
  • printf:将格式化字符串写入到标准输入文件
  • fprintf:将多个字符串,格式化为一个,写入指定文件
  • sprintf:将字符串格式化后,放入buffer中
  • snprintf:对sprintf的一个保护,将字符串拷贝至buffer时,给定大小的限制
  • fgets:获取一行数据
  • FILE*:文件流指针
#include <stdio.h>
#include <string.h>

int main(){
    FILE *fp=NULL;
    fp = fopen("./tmp.txt","w+");
    if(fp==NULL){
		perror("fopen error");
		return -1;
    }
	
    char buf[1024]="c'est la vie~\n";
    //size_t fwrite(void *ptr,size_t size,size_t nmemb,FILE *stream);
    fwrite(buf,strlen(buf),1,fp);
	
    //跳转读写位置
    //int fseek(FILE *stream,long offset,int whence);
    fseek(fp,0,SEEK_SET);
	
    //初始化buf内存
    memset(buf,0x00,1024);
    //size_t fread(void *ptr,size_t size,size_t nmemb,FILE *stream);
    int ret=fread(buf,1024,1,fp);
    //在文件末尾
    if(feof(fp)){
		printf("at end of file\n");
    }else{
		perror("fread error");
    }
    printf("ret:%d--buf:[%s]\n",ret,buf);
    fclose(fp);
    return 0;
}

2. 系统调用的IO接口 

  • open
    • int open(const char *pathname,int flags,mode_t mode);
      • pathname:打开的文件路径名
      • flags:选项参数
        • 必选其一
          • O_RDONLY     只读
          • O_WRONLY    只写
          • O_RDWR        读写
        • 可选项
          • O_CREAT       文件存在则打开,不存在则创建
          • O_TRUNC      将文件长度截断为0(清空原有内容)
          • O_APPEND    追加
      • mode:权限(八进制数字)  mode & ~umask
      • 返回值
        • 成功 -> 文件描述符(正整数,系统调用接口的操作句柄)
        • 失败 -> -1
  • write
    • ssize_t write(int fd,const void *buf,size-t count);
      • fd:open返回的文件描述符
      • buf:要写入的数据
      • count:写入的数据长度
      • 返回值
        • 成功 -> 实际写入的数据长度(字节) 
        • 失败 -> -1
  • read
    • ssize_t read(int fd,void *buf,size_t count);
      • fd:open返回的文件描述符
      • buf:内存首地址,用于获取存储读取的数据
      • count:读取的数据长度
      • 返回值
        • 成功 -> 实际读取的数据长度(字节) 
        • 失败 -> -1
  • close
    • int close(int fd);
  • lseek
    •  off_t lseek(int fd, off_t offset, int whence);
    • fd:open返回的文件描述符
    • offset:偏移量
    • whence:偏移起始位置
      • SEEK_SET:从文件起始位置开始偏移
      • SEEK_CUR:从当前读写位置开始偏移
      • SEEK_END:从文件末尾位置开始偏移
    • 返回值:成功 -> 从文件起始位置到当前读写位置的偏移量  失败 -> -1
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

int main(){
    //mode_t umask(mode_t mask);
    //设置调用进程的文件创建权限掩码
    umask(0);
	
    //int open(const char *pathname,int flags,mode_t mode);
    //不存在则创建时没有权限0664,为了确保实际权限时0664,设置umask为0
    int fd=open("./test.txt",O_RDWR|O_CREAT|O_APPEND|O_TRUNC,0664);
	
    if(fd<0){
		perror("open error");
		return -1;
    }
    char *ptr="la vie est belle.\n";

    int ret=write(fd,ptr,strlen(ptr));
    if(ret<0){
		perror("write error");
		return -1;
    }
    lseek(fd,0,SEEK_SET);
    char buf[1024]={0};
	
    //ssize_t read(int fd,void *buf,size_t count);
    ret=read(fd,buf,1023);
    if(ret<0){
		perror("read error");
		return -1;
    }
    printf("read buf:[%d-%s]\n",ret,buf);
    close(fd);
    return 0;
}

3. 文件描述符

  • 3.1 什么是文件描述符

 

  • 进程通过struct file结构体来描述打开的文件 -> struct file *fd_array[]
    • 文件描述符就是struct file结构体指针数组的下标
      • 用户打开文件,操作系统通过file结构体描述文件,并且将指针添加进入fd_array中,向用户返回这个文件描述信息在数组中的位置(下标)
      • 用户操作文件时,将这个下标传递给操作系统,操作系统通过下标找到文件描述信息进而操作文件。
    • fd_array[]在pcb的struct files_struct结构体中
  • 解释
    • 该文件存储在硬盘中,当进程打开一个文件后,在进程中使用struct file结构体进行描述,将struct file放入fd_array[],给用户返回一个下标fd,拿着fd去struct files_struct结构体中的fd_array[],通过fd找到文件,进而操作文件。
  • 进程的文件描述符是有上限的
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main(){
    close(1);
    int fd=open("./test.txt",O_RDWR);
    if(fd<0){
		perror("open error");
		return -1;
    }
    //\n刷新缓冲区,仅仅针对标准输出文件才有效
    //其他文件,\n仅仅具备换行功能
    printf("fd:%d\n",fd);
    fflush(student);
    //close是系统调用接口,没有缓冲区,直接关闭,数据没有刷新至文件中
    //我们所说的缓冲区是stdout的缓冲区(用户态的缓冲区)
    close(fd);
    return 0; 
}
  • 3.2 文件描述符的分配规则

    • 最小未使用原则
      • 在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。 
      • 默认从3开始 
        • ∵ 一个进程运行起来后,默认会打开三个文件
          • 标准输入  0   STDIN_FILENO
          • 标准输出  1   STDOUT_FILENO
          • 标准错误  2   STDERR_FILENO
  • 3.3 文件流指针与文件描述符的关系

    • 文件流指针
      • 文件库函数的操作句柄
        • stdin、stdout、stderr
    • 文件操作符
      • 系统调用接口的句柄
    • 文件流指针这个FILE结构体中就包含了一个文件描述符成员变量
      • 解释:
        • 打开文件test.txt,通过struct file结构体进行描述,将struct file放入fd_array[],给用户返回一个下标fd,通过fd进行操作。系统调用使用File *fp,通过fwrite(fp)写入数据,假设其调用write(fd),需要通过fp找到fd
  • 3.5 缓冲区位置

系统调用接口没有缓冲区,标准库中实现的缓冲区,fflush(stdout)刷新标准输出缓冲区。文件流指针结构体中描述了一个缓冲区,File *fp中有两个缓冲区(写入缓冲区、读取缓冲区)。

  • 3.6 通过文件流指针操作一个文件

    • 写入数据(用户态缓冲区):
      • 写入到文件流指针包含的缓冲区中,刷新缓冲区或缓冲区满了的时候,将数据写入文件。即开始真正的操作文件
      • 使用系统调用接口
      • 系统调用接口通过fp找到fd
      • 通过fd寻找文件描述信息
      • 通过文件描述信息去操作文件
      • 实现将数据写入到文件中   
/*缓冲区*/
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main(){

    printf("%s","printf");
    fprintf(stdout,"%s","fprintf");
    fwrite("fwrite",6,1,stdout);

    write(1,"write",5);
    sleep(3);
    return 0;
}
  • 3.7 内核态与用户态

    • 进程运行在用户态 / 进程运行在内核态:
      • 当我们发起系统调用调用某个硬件去完成某个系统功能,这些功能需要操作系统完成 -> 进程运行在内核态
      • 当前操作没有发起系统调用,当前的系统功能并非是由系统内核完成 -> 进程运行在用户态
    • 进程如何从用户态切换到内核态运行  ->  发起系统调用,使用系统调用接口

4. 重定向

改变数据流向,将写入指定文件的数据改变之后写入到另一个文件。

  • 本质
    • 描述符的重定向 -> 描述符并没有改变,改变的是描述符这个下标所对应的文件,描述信息
    • 操作相同的描述符,但是具体操作的文件已经改变
  • 4.1 dup2

    • int dup2(int oldfd, int newfd);
    • 功能
      • newfd本身指向b.txt,经过dup2接口后,newfd指向了a.txt,并且关闭b.txt。这时,oldfd和newfd指向的是同一个文件a.txt。
      • 即,让newfd也指向oldfd所指向的文件,并且如果newfd本身已经打开了文件,则关闭原先打开的文件。这时,oldfd和newfd操作的都是oldfd所指向的文件a.txt。
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main(){
    int fd=open("./test.txt",O_RDWR);
    if(fd<0){
		perror("open error");
		return -1;
    }
    //将要打印的数据不写入标准输出,而是写入test.txt
    //1是标准输出
    dup2(fd,1);

    //\n刷新缓冲区,仅仅针对标准输出文件才有效
    //其他文件,\n仅仅具备换行功能
    printf("fd:%d\n",fd);
    fflush(student);
    //close是系统调用接口,没有缓冲区,直接关闭,数据没有刷新至文件中
    //我们所说的缓冲区是stdout的缓冲区(用户态的缓冲区)
    close(fd);
    return 0; 
}
  • 4.2 在minishell中添加重定向

    • 5.2.1 重定向方式
      • >    清空原有内容
      • >>  追加新内容 
    • 5.2.2 实现ls -l >>       tmp.txt
      • 1.检索命令中是否有重定向功能
      • 2.命令分隔[ls -l] [tmp.txt] (将>>转换为/0)
      • 3.判断重定向方式
        • >          O_TRUNC
        • >>        O_APPEND
      • 4.创建子进程
      • 5.子进程实现重定向 -> 命令解析 -> 程序替换
      • 6.父进程等待子进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>

char buf[1024]={0};
char *argv[32];
int argc=0;

//显示信息,获取字符串
int do_face(){
    //等待标准输入
    printf("[qqy@localhost]$ ");
    fflush(stdout);
    //%[^\n] 从标准输入缓冲区中获取一个字符串,遇到换行为止
    %*c 将一个字符从缓冲区中取出(丢弃缓冲区中的/n)
    !=1代表buf中没有获取到数据
    if(scanf("%[^\n]%*c",buf)!=1){
		getchar(); //将回车取出
		return -1;
    }
    return 0;
}

//解析字符串
int do_parse(){
    argc=0;
    //buf[    ls   -a]如何解析
    //ls是程序名称,也是第0个参数
    //-a是第1个参数
    //检测空白字符
    //
    char *ptr=buf;
    //直到字符串结束
    while(*ptr!='\0'){
		//当前位置是非空白字符
		if(!isspace(*ptr)){
			argv[argc++]=ptr;
			//不是空白符号,也没有结束
			while(!isspace(*ptr) && *ptr!='\0'){
			ptr++;
			} 
		}
		//设置字符串结束标志
		*ptr='\0';
		ptr++;
    }
    //参数需要用NULL结尾
    argv[argc]=NULL;
    return 0;
}

//判断内建命令
int build_in(){
    //内建命令,shell内部实现其功能,无需创建子进程
    if(strcmp(argv[0],"cd")==0){
		//int chdir(const char *path);
		//改变当前路径
		chdir(argv[1]);
		return -1;
    }
    return 0;
}

//判断重定向
int redirect_flag=0;
char *redirect_file=NULL;
int do_redirect(){
    //ls -l     >>          tmp.txt
    redirect_flag=0;
    redirect_file=NULL;
    char *ptr=buf;
    while(*ptr !='\0'){
		//判定是否为>
		if(*ptr == '>'){
			redirect_flag=1;
			*ptr='\0';
			ptr++;
			//判定是否为>>
			if(*ptr=='>'){
				redirect_flag=2;
				*ptr='\0';
				ptr++;
			}
			//走到文件名
			while(*ptr!='\0' && isspace(*ptr)){
				ptr++;
			}
			//指向文件名
			redirect_file=ptr;
			//将文件名后面的空格走完
			while(*ptr != '\0' && !isspace(*ptr)){
				ptr++;
			}
			*ptr='\0';
		}
		ptr++;
    }
    return 0;
}

int main(){
    int ret=0;
    while(1){
		ret=do_face();
		if(ret<0){
			continue;
		}
		ret=do_redirect();
		if(ret<0){
			continue;
		}
		ret=do_parse();
		if(ret<0){
			continue;
		}
		ret=build_in();
		if(ret<0){
			continue;
		}

		//创建子进程,执行任务
		int pid=fork();
		//创建子进程失败
		if(pid<0){
			continue;
		}else if(pid==0){
			//子进程,执行其他任务 -> 程序替换
			int fd=1;
			if(redirect_flag == 1){
				//清空重定向
				fd=open(redirect_file,O_WRONLY|O_CREAT|O_TRUNC,0664);
				if(fd<0){
					exit(-1);
				}
			}else if(redirect_flag==2){
				//追加重定向
				fd=open(redirect_file,O_WRONLY|O_CREAT|O_APPEND,0664);
				if(fd<0){
					exit(-1);
				}
			}
			//将标准输出重定向
			dup2(fd,1);
			if(execvp(argv[0],argv)<0){
				perror("");
			}
			//替换失败 -> 退出
			exit(0);
		}
		//父进程等待子进程退出,避免出现僵尸进程
		wait(NULL);
    }
    return 0;
}

5. 文件系统(ext2)

磁盘上的文件管理系统

  • 除了swap交换分区,每一个磁盘分区都会有一个文件系统
  • 包含
    • linux-ext2-超级块、inode块、数据块、inode_bitmap\data_bitmap
  • 目录文件
    • 一个用于记录目录下的文件信息(文件名、inode节点号 -> 目录项)的普通文件
  • 5.1 文件存储与读取

    • 在分区中存储文件,一个一个的进行存储,很快就会将磁盘存满。只剩一点点空间时,无法存储大文件。磁盘利用率过低,查找效率低。
    • 因此,进行磁盘分块管理,每一块4096个字节。
  • 5.1.1 inode节点
    • 若遇到超过4096字节的文件,将文件分块存储,可能会存到不同的磁盘块。因此,需要使用inode节点记录数据存储位置。
    • 每个文件都有一个inode节点去记录描述文件信息
    • 内容
      • 大小
      • 权限
      • 时间
      • 数据存储地址
    • inode节点存储于存储文件的inode节点区域
    • 查看inode节点大小
      • sudo dumpe2fs -h/dev/sda1|grep "Inode size"
  • 5.1.2 位图区域
    • data_bitmap,标记数据块的使用情况
    • inode_bitmap,标记inode的使用情况
  • 5.1.3 超级块
    • 格式化文件系统、分区时,在超级块中记录分区的文件系统类型、inode节点个数和起始地址、数据块个数和起始地址等统筹信息。
  • 5.1.4 文件存储过程
    1. 通过inode_bitmap在inode区域获取空闲inode节点

    2. 通过data_bitmap获取空闲数据块

    3. 在inode节点中记录文件信息以及数据块位置

    4. 将文件数据写入到数据块中

    5. 将自己的目录项信息添加到所在目录文件中

  • 5.1.5 文件存储过程
    1. 通过文件名在目录项中获取文件inode节点号(文件唯一)

    2. 通过inode节点号在inode区域找到inode节点

    3. 通过inode节点中的数据块地址信息,在指定数据块读取数据

    4. 将文件数据写入到数据块中

    5. 将自己的目录项信息添加到所在目录文件中

  • 5.2 软链接与硬链接

  • 5.2.1 软链接文件
    • 创建
      • ln -s srcfile  destfile
    • 软链接文件与源文件的inode节点号不同,其数据区域存储的是源文件的路径,通过路径找到软链接文件的目录项,通过目录项获取源文件的节点号,进而操作源文件。
    • 相当于文件快捷方式,是一个独立的文件
  • ​​​​​5.2.2 硬链接文件
    • 创建
      • ln srcfile destfile
    • 硬链接文件与源文件的inode节点号相同,表明硬链接文件只是一个目录项,而不是一个实质性的文件。
    • 相当于文件名(目录项),与原文件共用同一个inode节点
  • 5.2.3 区别
    • 删除源文件,软链接文件将失效(通过文件名查找源文件);硬链接无影响(通过inode节点找文件,链接数-1)
    • 软链接可以跨分区创建,硬链接不可以(软链接只针对文件名,而硬链接需要inode节点)
    • 软链接可以对目录创建,硬链接不可以(目录结构是跨分区的)

6. 动态库与静态库

  • 6.1 动态库的生成

    • 命名
      • lib是前缀
      • .so是后缀
      • 中间是库名称
    • 命令
      • gcc -fPIC -c child.c -o child.o
        • -fPIC:产生位置无关代码
      • gcc --share child.o -o libmychild.so
        • --share:指定gcc要生成动态库而不是可执行程序
  • 6.2 静态库的生成

    • 命名

      • lib是前缀
      • .a是后缀
      • 中间是库名称
    • 命令
      • gcc -c child.c -o child.o
      • ar -cr libmychild.a child.o
        • ar:静态库打包所用命令
        • -c:创建
        • -r:模块替换
  • 6.3 库的使用

    • gcc main.c -o main -lmychild 
      • -l 指定链接的库名称
      • 报错,因为链接库时,库文件有默认的搜索路径 /lib64/lib/usr/lib64...
      • 解决
        • 将库文件放置到指定目录下  /lib64
        • 设置环境变量
          • 库文件的链接搜索路径:LIBRARY_PATH=.
          • 库文件的运行加载路径:LD_LIBRARY_PATH=.
        • 在gcc生成可执行程序时,直接指定库的搜索路径
          • gcc main.c -o main -L . -lmychild
            • gcc
              • -L:指定库的链接搜索路径
              • -l(小L):指定链接库的名称
              • -I(大i):指定头文件搜索路径
      • 使用第三方静态库,不能使用gcc的-static选项
        • -static选项是生成静态链接可执行程序,所有的库都使用静态库(是希望第三方库使用静态库,而不是所有)
        • 解决
          • 将第三方静态库拷贝到指定路径下,使用-L选项指定库的链接搜索路径。这时,链接的就是静态库。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值