- 学习地址:bilibili
- 课程名称:《嵌入式之Linux下文件I/O精讲》-华清远见出品-曾老师
- 课程链接
20200210-20200221
文件基础
概念
- 一组相关数据的有序集合
文件类型
Linux中7中不同的文件类型,不同的操作系统所支持的文件类型是不同的
- 常规文件 r
- 目录文件 d
- 字符设备文件 c
- 块设备文件 c
- 管道文件 p
- 套接字文件 s
管道文件和套接字文件都是进程间通信的一种机制;套接字既可以进行网络通信也可以用于本地通信
- 符号链接文件 l
文件I/O介绍
裸机硬件系统:如单片机,没有操作系统,代码可以直接操作物理硬件,通过地址操作
有操作系统设备:
标准IO通过内部的缓冲机制来减少系统调用的次数
Linux系统中I/O操作有两种方法:一种是标准I/O,另一种时文件I/O
标准I/O与文件I/O对比
- 标准I/O遵循的是C的标准-ANSI
- 标准I/O是带缓冲的流,系统会分配缓冲区;首先操作的是缓冲区,减少系统调用的次数
- 标准I/O通过流(FILE)来表示打开的文件
- 文件I/O遵循的是POSIX的标准(POSIX是操作系统的规范)
- 文件I/O是无缓冲的, 每次的读写都是相应的系统调用去读写实际的文件
- 文件I/O通过文件描述符(fd )来表示打开的文件
标准I/O流
FILE
- 就是FILE结构体 FILE:
- 标准I/O用一个结构体类型来存放打开的文件的相关信息
- 标准I/O的所有操作都是围绕FILE来进行的
流(stream)
- FILE又被称为流
- 文本流/二进制流
Windows区分文本流和二进制流
Linux不区分,实际上都是二进制流
流的缓冲类型
标准I/O是带缓冲的I/O;先对缓冲进行操作
- 全缓冲
当流的缓冲区无数据或无空间时才执行I/O操作
- 行缓冲
当在输入和输出中遇到换行符(’\n’)时,进行I/O操作
eg:当流和一个终端相关联时,就是典型的行缓冲
- 无缓冲
数据直接写入文件,流不进行缓冲
打开流
下列的函数用于打开一个标准的I/O流
FILE *fopen(const char *path,const char *mode);
成功时返回流的指针,错误时返回NULL
mode参数
打开一个标准I/O的六种不同方式
新建文件权限
- fopen创建的文件访问权限是666(
rw-rw-rw-
) - linux系统中umask设定会影响文件的访问权限,其规则为(
0666&~umask
) - 用户可以通过umask函数修改相关设定
- 如果不希望umask影响我们文件的访问权限,可以将
umask设定为0
处理错误信息
关闭流
int fclose(FILE *stream)
- fclose()调用成功后返回0,失败返回EOF,并设置error
- 流关闭时自动刷新缓冲区中的数据并释放缓冲区
- 当一个程序正常终止时,所有打开的流都会被关闭
建议当流不使用的时候及时主动关闭
- 流一旦关闭就不能执行任何操作
读写流
流支持不同的读写方式
- 读写一个字符:
fgetc()/fputc()
一次读/写一个字符 - 读写一行:
fgets()/fputs()
一次读/写一行,一般处理文本流 - 读写若干个对象:
fread()/fwrite()
每次读写若干个对象,而每个对象具有相同的长度,既可以处理文本流也可以处理二进制流
按字符输入
下列函数用来输入字符
#include<stdio.h>
int fgetc(FILE *stream);
int getc(FILE *stream);
int getchar(void);
成功时返回读取到的字符;若到文件末尾或出错时返回EOF
fgetc(stdin)等同于getchar()
按字符输出
下列函数用来输出字符
#include<stdio.h>
int fputc(int c,FILE *stream);
int putc(int c,FILE *stream);
int putchar(int c);
成功时返回写入的字符;出错时返回EOF
putchar()
默认向标准输出流写入一个字符
fputc(int c,stdout)等同于putchar()
diff -ruN srcfile1 desfile2
可以比较差别
按行输入
下面的函数用来输入一行
#include<stdio.h>
char *gets(char *s);
char *fgets(char *s,int size,FILE *stream);
gets()
从标准输入流读取一行放到以s为首地址的内存缓冲区中;不建议使用,容易造成缓冲区溢出,不安全
fgets()函数从指定流中读取size
长度字符到以s为首地址的内存缓冲区中
成功时返回缓冲区的首地址s
;到文件末尾或错误时返回NULL
遇到'\n'
或已输入size-1
个字符时返回,总是在指定缓冲区末尾包含'\0'
;
若先遇到'\n'
,则结尾是'\n''\0'
若先满足size-1
,则结尾是'\0'
按行输出
下列函数用来输出字符串
#include<stdio.h>
char puts(const char *s);
char fputs(const char *s,FILE *stream);
puts()
以s为首地址的内存缓冲区中放到标准输入流
puts()
将缓冲区s中的字符串输出到stdout
,并追加'\n'
fputs()
函数从以s为首地址的内存缓冲区中写入到指定的流中
fputs()
将缓冲区s中的字符串输出到stream
成功时返回输出的字符个数;错误时返回EOF
按指定对象输入
下列函数用于从流中读取若干个对象
#include<stdio.h>
size_t fread(void *ptr,size_t size,size_t n,FILE *fp);
size_t fwrite(const void *ptr,size_t size,size_t n,FILE *fp);
成功时返回实际读写的对象的个数;错误时返回EOF
fread()
如果读到文件末尾返回的是0
既可以读写文本文件;也可以读写数据文件
按指定对象输出
流的刷新
全缓冲:当缓冲区满时进行刷新数据到流
行缓冲:当缓冲区满或者遇到'/n'
进行刷新数据到流
下列函数用来主动刷新流
#include<stdio.h>
int fflush(FILE *fp);
成功时返回
0
;出错时返回EOF
将流的缓冲区中的数据写入实际的文件
linux下只能刷新输出缓冲区到文件
流的定位
每个流在打开的时候都会记录一个读写位置;流的定位实际上和这个读写位置有关
ftell/fseek/rewind
#include<stdio.h>
//获取当前流的读写位置
long ftell(FILE *stream);
//设定流的当前读写位置whence是基准点,offset相对于基准点的偏移
long fseek(FILE *stream,long offset,int whence);
//直接把流定义到起始位置
void rewind(FILE *stream);
ftell()
成功时返回的时流的当前读写位置;出错时返回EOF(-1)
fseek()
定位一个流,成功时返回0;出错时返回EOF(-1)
whence
参数:SEEK_SET/SEEK_CUR/SEEK_END
->文件的开始位置/文件的当前位置/文件的末尾
读写流时,当前读写位置都会自动后移
检测流结束和出错
#include<stdio.h>
int ferror(FILE *stream);
int feof(FILE *stream);
ferror()
返回1表示流出错;否则返回0
feof()
返回1表示文件已到末尾;否则返回0
格式化输出
sprintf/fprintf
#include<stdio.h>
int printf(const char *fmt,...);
//向指定流(文件)输出
int fprintf(FILE *stream,const char *fmt,...);
//以指定的格式把字符串输出到缓冲区s中
int sprintf(char *s,const char *fmt,...);
- 成功时返回输出的字符个数;出错时返回EOF
文件I/O
如何理解文件I/O
文件I/O总结
文件描述符的含义
- 每个打开的文件都有一个对应的文件描述符
- 文件描述符是一个
非负整数
;Linux系统为每个打开的文件分配一个文件描述符 - 文件描述符从
0
开始,依次递增 - 每个程序打开的文件所对应的文件描述符都是独立的
- 文件I/O通过文件描述符来完成
- 由于Linux下的标准I/O是通过对文件I/O做的带缓冲的封装实现的;所以
0、1、2对应stdin/stdout/stderr的文件描述符
文件的打开和关闭
一个程序中默认打开的文件是有上限的,一般是
1024
open()
函数用来打开或者创建一个文件
#include <fcntl.h>
int open(const char *path,int oflag,...);
成功时返回文件描述符;错误时返回EOF
打开一个已经存在的文件时用两个参数
创建文件时,第三个参数指定新建文件的访问权限
设备文件
只能通过open()
打开,不能通过open()
创建
open()函数参数解析
close()
函数用来关闭一个打开的文件
#include <unisd.h>
int close(int fd);
成功时返回0;出错时返回EOF
文件关闭时,文件描述符不再代表文件
读取文件
read()函数用于从文件中读取数据
#include <unistd.h>
ssize_t read(int fd,void *buf,size_t count);
第二个参数buf是缓冲区的首地址,用来接收数据;需要先提前申请分配好
第三个参数不能超过缓冲区的大小,否则溢出;一般指定为缓冲区大小
成功时返回实际读取的字节数;出错时返回EOF
读到文件末尾时返回0
;可以判断是否读完文件
写入文件
write()函数用于从文件中读取数据
#include <unistd.h>
ssize_t write(int fd,void *buf,size_t count);
成功时返回实际写入的字节数;出错时返回
EOF
buf
是发送数据的缓冲区
count
不应超过buf
大小
定位文件
lseek()函数用来定位文件
#include <unistd.h>
off_t lseek(int fd,off_t offset,int whence);
成功时返回当前文件的读写位置;出错时返回
EOF
参数offset
和whence
与fseek()
的参数一样
读取目录
opendir()函数用来打开一个目录文件
#include <dirent.h>
DIR *opendir(const char *name);
DIR是用来描述一个打开的目录文件的结构体类型
成功时返回目录流指针;出错时返回NULL
readdir()函数用来读取目录流中的内容
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
struct dirent
是用来描述目录流中一个目录项的结构体类型
包含成员char d_name[256]
参考帮助文档
成功时返回目录流dirp
中的下一个目录项
出错或到末尾时返回NULL
closedir()函数用来关闭一个目录文件
#include <dirent.h>
int *closedir(DIR *dirp);
成功时返回0;出错时返回EOF
修改文件访问权限
chmod()/fchmod()函数用来修改文件的访问权限
#include <sys/stat.h>
int chmod(const char *path,mode_t mode);
int fchmod(int fd,mode_t mode);
成功返回0;错误返回EOF
root用户和文件所有者可以修改文件的权限
eg:chmod("text.txt",0666);
获取文件属性
stat()/lstat()/fstat()函数用来获取文件的属性
#include <sys/stat.h>
int stat(const char *path,struct stat *buf);
int lstat(const char *path,struct stat *buf);
int fstat(int fd,struct stat *buf);
获取的属性存储在结构体指针
struct stat*
里
成功返回0;错误返回EOF
如果path时符号链接stat获取的是目标文件的属性;而lstat获取的是链接文件的属性
程序库的概念
- 库是一个二进制的文件,包含编译好的代码;可被其它程序调用
- 标准C库、数学库、线程库…
- 库有源码,可下载后编译,也可以直接安装二进制包
- 一般系统默认库路径
/lib
和/usr/lib
- 库是事先编译好的,可以复用的代码
- 在OS上运行的程序基本上都要使用库,使用库可以提高开发效率
- Windows和linux下库文件的格式不兼容
- Linux下包含静态库和共享库
静态库
静态库的特点
- 链接时把静态库中的相关代码复制到可执行文件中
- 程序中已包含所需代码,运行时不在需要静态库
- 程序运行时无需加载库,运行速度快
- 占用更多的磁盘和内存空间
- 静态库升级后,程序需要重新编译
静态库的创建
- 确定库中函数的功能和接口(参数和返回值)
- 编写库源码 (
static.c
) - 编译生成
.o
目标文件 (gcc -c static.c -Wall
) - 创建静态库,可以
多个.o
(ar crs libstatic.a static.o
)
注意:Linux中静态库的名称是有要求的,一般是以
lib + 函数名或功能名 +.a
- 查看库中的符号信息 (
nm libstatic.a
) - 编写应用程序(
test.c
)测试调用库文件(注意声明)
链接静态库
- 编译
test.c
并链接静态库libstatic.a
(gcc -o test test.c -L. -lstatic
)
-L
指定库的搜索路径,本例是当前目录
-l
指定要链接库的名称,注意是库名而不是库文件名
共享库
共享库概念
- 链接时仅记录用到哪个共享库中的哪个符号(即函数),不复制共享库中的代码
- 程序不包含共享库中的代码,尺寸更小
- 多个程序可共享同一个共享库
- 程序运行时需要加载共享库
- 库升级方便,无需重新编译程序
- 使用更加广泛
共享库创建
- 确定库中函数的功能、接口(参数和返回值)
- 编写库源码 (
share.c bye.c
) - 编译生成
.o
目标文件 (gcc -c -fPIC share.c bye.c -Wall
) - 创建共享库
common
(gcc -shared -o libcommon.so.1 share.o bye.o
) - 为共享库文件创建链接符号文件 (
ln -s libcommon.so.1 libcommon.so
)
-fPIC
告诉编译器生成与位置无关目标文件代码,用到相对寻址;即生成.o代码
可以被加载到任意地址执行,而不是固定的地址
1
是共享库的版本
符号链接文件命名规则 (lib<库名>.so
)
- 编写应用程序(
testshare.c
)测试调用库文件(添加声明或者单独制作头文件,把声明放在头文件中,本例中制作单独头文件common.h
在测试程序中添加#include "common.h"
)
链接共享库
- 编译
testshare.c
并链接共享库libcommon.so
(gcc -o testshare testshare.c -L. -lcommon
)
gcc编译器默认先搜索共享库,如果想直接使用静态库,添加
-static
参数
加载共享库
如何找到共享库,为了能让系统找到要加载的共享库,有三种方法:
- 把共享库拷贝到
/lib
和/usr/lib
目录下,不建议此方法(系统默认的库的搜索路径) - 在LD_LIBRARY_PATH环境变量中添加库所在的路径(仅对当前shell有效) eg:添加当前目录到环境变量
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
- 添加
/etc/ld.so.conf.d/*.conf
文件,执行ldconfig
刷新
以
my.config
为例,很简单就是把自己制作的动态库所在路径添加到my.config
里;然后执行ldconfig