Linux系统与文件操作相关的应用程序结构

上图解构如下:
- 应用层指用户编写的程序,如我们的hello.c。
- GNU C库(glibc)即C语言标准库,例如在编译器章节介绍的libc.so.6文件,它 包含了printf、malloc,以及fopen、fread、fwrite等文件操作函数。
- 用户程序和glibc库都是属于用户空间的,本质都是用户程序。
- 应用层的程序和glibc可能会调用到“系统调用层(SCI)”的函数,这些函数 是Linux内核对外提供的函数接口,用户通过这些函数向系统申请操作。例如,C库的printf函数使用了系统的vsprintf和write函数,C库的fopen、fread、fwrite分别调用了系统的open、read、write函数,具体可以阅读glibc的源码了解。
- 由于文件系统种类非常多,跟文件操作相关的open、read、write等函数经过虚拟文件系统层,再访问具体的文件系统。
总的来说,为了使不同的文件系统共存, Linux内核在用户层与具体文件系统之前增加了虚拟文件系统中间层,它对复杂的系统进行抽象化,对用户提供了统 一的文件操作接口。无论是ext2/3/4、FAT32、NTFS存储的文件,还是/proc、/sys提供的信息还是硬件设备,无论内容是在本地还是网络上,都使用一样的open、read、write来访问,使得“一切皆文件”的理念被实现,这也正是软件中间层的魅力。
Linux系统调用
从上图可了解到,系统调用(System Call)是操作系统提供给用户程序调用的一组“特殊”函数接口API,文件操作就是其中一种类型。实际上,Linux提供的系统调用包含以下内容:
- 进程控制:如fork、clone、exit 、setpriority等创建、中止、设置进程优先级的操作。
- 文件系统控制:如open、read、write等对文件的打开、读取、写入操作。
- 系统控制:如reboot、stime、init_module等重启、调整系统时间、初始化模块的系统操作。
- 内存管理:如mlock、mremap等内存页上锁重、映射虚拟内存操作。
- 网络管理:如sethostname、gethostname设置或获取本主机名操作。
- socket控制:如socket、bind、send等进行TCP、UDP的网络通讯操作。
- 用户管理:如setuid、getuid等设置或获取用户ID的操作。
- 进程间通信:包含信号量、管道、共享内存等操作。
从逻辑上来说,系统调用可被看成是一个Linux内核与用户空间程序交互的中间人,它把用户进程的请求传达给内核,待内核把请求处理完毕后再将处理结果送回给用户空间。它的存在就是为了对用户空间与内核空间进行隔离,要求用户通过给定的方式访问系统资源,从而达到保护系统的目的。
也就是说,我们心心念念的Linux应用程序与硬件驱动程序之间,就是各种各样的系统 调用,所以无论出于何种目的,系统调用是学习Linux开发绕不开的话题。
下面通过文件操作的两个实验,来演示使用C标准库与系统调用方式的差异。
文件操作(C标准库)
我们先使用通用的C标准库接口访问文件,标准库实际是对系统调用再次进行了封装。使用C标准库编写的代码,能方便地在不同的系统上移植。
例如Windows系统打开文件操作的系统API为OpenFile,Linux则为open,C标准库都把它们封装为fopen,Windows下的C库会通过fopen调用OpenFile函数实现操作,而Linux下则通过glibc调用open打开文件。用户代码如果使用fopen,那么只要根据不同的系统重新编译程序即可,而不需要修改对应的代码。
在开发时,遇到不熟悉的库函数或系统调用,要善用man手册,而不要老是从网上查找。
C标准库提供的常用文件操作简介如下:
fopen库函数用于打开或创建文件,返回相应的文件流。它的函数原型如下:
#include <stdio.h>
FILE *fopen(const char *pathname, const char *mode);
- pathname参数用于指定要打开或创建的文件名。
- mode参数用于指定文件的打开方式,注意该参数是一个字符串,输入时需要带双引号:
- “r”:以只读方式打开,文件指针位于文件的开头。
- “r+”:以读和写的方式打开,文件指针位于文件的开头。
- “w”:以写的方式打开,不管原文件是否有内容都把原内容清空掉,文件指针位于文件的开头。
- “w+”: 同上,不过当文件不存在时,前面的“w”模式会返回错误,而此处的“w+”则会创建新文件。
- “a”:以追加内容的方式打开,若文件不存在会创建新文件,文件指针位于文件的末尾。与“w+”的区别是它不会清空原文件的内容而是追加。
- “a+”:以读和追加的方式打开,其它同上。
- fopen的返回值是FILE类型的文件文件流,当它的值不为NULL时表示正常,后续的fread、fwrite等函数可通过文件流访问对应的文件。
fread库函数用于从文件流中读取数据。它的函数原型如下:
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
stream是使用fopen打开的文件流,fread通过它指定要访问的文件,它从该文件中读取nmemb项数据,每项的大小为size,读取到的数据会被存储在ptr指向的数组中。fread的返回值为成功读取的项数(项的单位为size)。
fwrite库函数用于把数据写入到文件流。它的函数原型如下:
#include <stdio.h>
size_t fwrite(void *ptr, size_t size, size_t nmemb, FILE *stream);
它的操作与fread相反,把ptr数组中的内容写入到stream文件流,写入的项数为nmemb,每项 大小为size,返回值为成功写入的项数(项的单位为size)。
fclose库函数用于关闭指定的文件流,关闭时它会把尚未写到文件的内容都写出。因为标准 库会对数据进行缓冲,所以需要使用fclose来确保数据被写出。它的函数原型如下:
#include <unistd.h>
int close(int fd);
fflush函数用于把尚未写到文件的内容立即写出。常用于确保前面操作的数据被写 入到磁盘上。fclose函数本身也包含了fflush的操作。fflush的函数原型如下:
#include <stdio.h>
int fflush(FILE *stream);
fseek函数用于设置下一次读写函数操作的位置。它的函数原型如下:
#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);
其中的offset参数用于指定位置,whence参数则定义了offset的意义,whence的可取值如下:
- SEEK_SET:offset是一个绝对位置。
- SEEK_END:offset是以文件尾为参考点的相对位置。
- SEEK_CUR:offset是以当前位置为参考点的相对位置。
示例代码及分析
#include <stdio.h>
#include <string.h>
//要写入的字符串
const char buf[] = "filesystem_test:Hello World!\n";
//文件描述符File类型是C语言预定义的一个结构体,他位于stdio.h中,是管理文件流的一种结构
FILE *fp;
char str[100];
int main(void)
{
//创建一个文件
fp = fopen("filesystem_test.txt", "w+");
//正常返回文件指针
//异常返回NULL
if(NULL == fp){
printf("Fail to Open File\n");
return 0;
}
//将buf的内容写入文件
//每次写入1个字节,总长度由strlen给出
fwrite(buf, 1, strlen(buf), fp);
//写入Embedfire
//每次写入1个字节,总长度由strlen给出
fwrite("Embedfire\n", 1, strlen("Embedfire\n"),fp);
//把缓冲区的数据立即写入文件
fflush(fp);
//此时的文件位置指针位于文件的结尾处,使用fseek函数使文件指针回到文件头
fseek(fp, 0, SEEK_SET);
//从文件中读取内容到str中
//每次读取100个字节,读取1次
fread(str, 100, 1, fp);
printf("File content:\n%s \n", str);
fclose(fp);
return 0;
}
如果之前有学习过C语言的文件操作,本实验代码非常容易理解,它的流程就是使用fopen创建文件、使用fwrite写入内容,使用fflush确保缓冲区的内容写到文件,然后使用fseek重置文件位置指针,使用fread把文件的内容读出,最后调用fclose关闭文件。
其中的fopen函数调用时使用了参数“w+”,表示每次都创建新的空文件,且带上读权限打开,函数调用后得到文件描述符fp,在它后面的fwrite、fread、fflush等函数都是通过这个fp文件描述符访问该文件的。
与文件操作(系统调用)中差异最大的就是此处fwrite和fread之间 的fflush函数,C标准库的文件系统带缓冲区,而系统调用的文件操作不带缓冲区,所以没有相应的flush函数。
makefile参考
#生成可执行文件的名称
Target = file_demo
ARCH ?= x86
#编译器CC
#根据传入的参数ARCH,确定使用的编译器
#默认使用gcc编译器
#make ARCH=arm 时使用ARM-GCC编译器
ifeq ($(ARCH), x86)
CC = gcc
else
CC = arm-linux-gnueabihf-gcc
endif
#存放中间文件的路径
build_dir = build_$(ARCH)
#存放源文件的文件夹
src_dir = .
#存放头文件的文件夹
inc_dir = includes .
#源文件
sources = $(foreach dir,$(src_dir),$(wildcard $(dir)/*.c))
#目标文件(*.o)
objects = $(patsubst %.c,$(build_dir)/%.o,$(notdir $(sources)))
#头文件
includes = $(foreach dir,$(inc_dir),$(wildcard $(dir)/*.h))
#编译参数
#指定头文件的路径
CFLAGS = $(patsubst %, -I%, $(inc_dir))
#链接过程
#开发板上无法使用动态库,因此使用静态链接的方式
$(build_dir)/$(Target) : $(objects) | create_build
$(CC) $^ -o $@
#编译工程
#编译src文件夹中的源文件,并将生成的目标文件放在objs文件夹中
$(build_dir)/%.o : $(src_dir)/%.c $(includes) | create_build
$(CC) -c $(CFLAGS) $< -o $@
#以下为伪目标,调用方式:make 伪目标
#clean:用于Clean Project
#check:用于检查某个变量的值
.PHONY:clean cleanall check create_build
#按架构删除
clean:
rm -rf $(build_dir)
#全部删除
cleanall:
rm -rf build_x86 build_arm
#命令前带"@",表示不在终端上输出执行的命令
#这个目标主要是用来调试Makefile时输出一些内容
check:
@echo $(CFLAGS)
@echo $(CURDIR)
@echo $(src_dir)
@echo $(sources)
@echo $(objects)
#创建一个新目录create,用于存放过程文件
create_build:
@mkdir -p $(build_dir)
makefile和main.c文件放在一个文件夹下
arm开发板输入
#在主机的实验代码Makefile目录下编译
#编译arm平台的程序
make ARCH=arm
编译后生成的ARM平台程序为build_arm/file_demo,使用网络文件系统共 享至开发板,在开发板的终端上运行即可,如下图所示。

