零、前言
本章主要讲解学习Linux基础IO流的知识
一、C语言文件IO
1、C库函数介绍
具体详解博文: 文件操作超详解CSDN博客
- 打关文件fopen/fclose:
FILE * fopen(const char* filename, const char* mode); int fclose (FILE* stream );
- 读写函数fread/fwrite:
size_t fread( void *buffer, size_t size, size_t count, FILE *stream ); size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
- 格式化读写fscanf/fprintf:
int fscanf( FILE *stream, const char *format [, argument ]... ); int fprintf( FILE *stream, const char *format [, argument ]...);
- 示例1:输出使用
#include <stdio.h> #include <string.h> int main() { const char *msg = "hello fwrite\n"; fwrite(msg, strlen(msg), 1, stdout); printf("hello printf\n"); fprintf(stdout, "hello fprintf\n"); return 0; }
- 示例2:文件读写
#include<stdio.h> #include<stdlib.h> #include<string.h> int main() { FILE* fp=fopen("cole.txt","w+");//以写读方式打开文件 if(fp==NULL) { perror("fopen"); exit(1); } char msg[]="linux so easy!\n"; int len=strlen(msg); int cnt=5; while(cnt--) { fwrite(msg,len,1,fp);//写入 } fseek(fp,0,SEEK_SET);//指针回到起始位置 char buf[128]; while(1) { ssize_t s=fread(buf,1,len,fp); if(s>0) { buf[s]=0;//设置结束符 fwrite(buf,len+1,1,stdout); } else//读取结束 break; } fclose(fp); return 0; }
2、stdin/stdout/stderr
- 文件原型:
extern FILE *stdin; extern FILE *stdout; extern FILE *stderr;
概念:
任何C程序运行都会默认打开三个输入输出流,分别是:stdin, stdout, stderr
分三个文件流分别对应键盘文件,显示器文件,显示器文件
为什么这里的文件流和外设关联上了:
对于所有外设硬件来说,其本质对应的操作不外乎是读操作和写操作,对于不同外设也就有不同的读写方式
OS要管理硬件设备无非是先描述再组织,由此将属性以及读写操作构成一个结构体,而文件其本身也是属性加读写操作,这样就由文件结构体同一管理文件(包括外设)
在C语言中虽然没有多态,但是结构体中可以储存函数指针,初始化结构体时,将属性写入的同时也将对应的读写函数给写入;对于外设来说,通过对应的文件结构体使用函数指针调用对应的读写函数,也就将数据刷新到对于设备上/从设备上读取数据
由此将普通文件和硬件设备管理组织好,所以对于Linux来说:一切皆文件
为什么C语言默认打开这三个输入输出流:
不仅仅是C语言会默认打开这三个输入输出流文件,几乎是任何语言都会这样,而这就不仅仅是语言层面上的功能了,也是由操作系统所支持的
对于任何语言来说,都有输入输出的需求,而不打开这三个输入出输出流文件,则无法使用这些接口
二、系统文件IO
1、系统调用介绍
操作文件,除了上述C接口(当然C++也有接口,其他语言也有),还可以使用系统接口
- open接口:
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode);
- 参数解释:
- pathname: 要打开或创建的目标文件
- flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags
- mode_t:如果没有对应文件需要进行创建的话,就需要指定创建文件的八进制访问权限值
注:这里的参数选项是依靠不同的比特位来标识对应的功能设定,所以这里的异或操作就是将对应比特位置为1,同时函数也是通过对每个比特位进行与操作检查是否该比特位置为了1
- 原型示例:
#define O_RDONLY 00 #define O_WRONLY 01 #define O_RDWR 02 #define O_CREAT 0100
- 其他接口:
int close(int fd); //使用close函数时传入需要关闭文件的文件描述符fd即可,若关闭文件成功则返回0,若关闭文件失败则返回-1 ssize_t write(int fd, const void *buf, size_t count); //使用write函数,将buf位置开始向后count字节的数据写入文件描述符为fd的文件当中 //如果数据写入成功,实际写入数据的字节个数被返回;如果数据写入失败,-1被返回 ssize_t read(int fd, void *buf, size_t count); //使用read函数,从文件描述符为fd的文件读取count字节的数据到buf位置当中 //如果数据读取成功,实际读取数据的字节个数被返回;如果数据读取失败,-1被返回
- 示例:文件读写
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<stdlib.h> #include<string.h> #include<unistd.h> int main() { int fd=open("cole",O_WRONLY|O_CREAT,0644); if(fd<0) { perror("open"); exit(1); } char msg[]="i like linux!\n"; int len=strlen(msg); int cnt=5; while(cnt--) { write(fd,msg,len); } close(fd); fd=open("bit",O_RDWR); char buf[128]; while(1) { ssize_t s=read(fd,buf,len); if(s>0) { buf[s]=0; write(1,buf,len+1); } else break; } close(fd); return 0; }
结果:
2、系统调用和库函数
概念:
对于上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc);而 open close read write lseek 都属于系统提供的接口,称之为系统调用接口
对于系统调用来说,接近底层,使用成本较高,并且不具备可移植性,只在本系统下可以,其他系统不行
对于库函数来说,是在系统暴露的接口上的一个二次开发(最终调用系统调用),在兼容自己语法的特性的同时,具有可移植性(自动根据平台选择自己底层对应的接口)
即可以认为库函数是对系统调用的封装,减低人工学习成本,方便二次开发
示图:
三、文件描述符
1、open返回值
- 文件描述符fd:
文件描述符就是一个小整数
- 0 & 1 & 2:
Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2
- 示例1:
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> #include <unistd.h> int main() { char buf[1024]; ssize_t s = read(0, buf, sizeof(buf));//标准输入 if(s > 0){ buf[s] = 0; write(1, buf, strlen(buf));//标准输出 write(2, buf, strlen(buf));//标准错误 } return 0; }
示例2
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> #include <unistd.h> int main() { int fd1=open("fd1.txt",O_WRONLY|O_CREAT,0644); int fd2=open("fd1.txt",O_WRONLY|O_CREAT,0644); int fd3=open("fd1.txt",O_WRONLY|O_CREAT,0644); int fd4=open("fd1.txt",O_WRONLY|O_CREAT,0644); int fd5=open("fd1.txt",O_WRONLY|O_CREAT,0644); printf("%d\n",fd1); printf("%d\n",fd2); printf("%d\n",fd3); printf("%d\n",fd4); printf("%d\n",fd5); return 0; }
注:从示例中可见,文件描述符就是从0开始的小整数:默认打开0,1,2,再打开则是从后递增
分析:
当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件
于是就有了file结构体,表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。
每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针
所以本质上,文件描述符就是该数组的下标。只要拿着文件描述符,就可以通过PCB到file_struct的指针数组找到对应的文件结构体地址
示图:
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