Linux应用层开发 · 2023年7月28日 0

Linux系统调用-文件操作(系统调用)

Linux提供的文件操作系统调用常用的有open、write、read、lseek、close等。

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);

Linux使用open函数来打开文件,并返回该文件对应的文件描述符。函数参数的具体说明如下:

  • pathname:要打开或创建的文件名;
  • flag:指定文件的打开方式,具体有以下参数,见下表 flag参数值。

表 flag参数值

标志位含义
O_RDONLY以只读的方式打开文件,该参数与O_WRONLY和O_RDWR只能三选一
O_WRONLY以只写的方式打开文件
O_RDWR以读写的方式打开文件
O_CREAT创建一个新文件
O_APPEND将数据写入到当前文件的结尾处
O_TRUNC如果pathname文件存在,则清除文件内容

C库函数fopen的mode参数与系统调用open的flags参数有如下表中的等价关系。

表 fopen的mode与open的flags参数关系

fopen的mode参数open的flags参数
rO_RDONLY
wO_WRONLY | O_CREAT | O_TRUNC
aO_WRONLY | O_CREAT | O_APPEND
r+O_RDWR
w+O_RDWR | O_CREAT | O_TRUNC
a+O_RDWR | O_CREAT | O_APPEND
  • mode:当open函数的flag值设置为O_CREAT时,必须使用mode参数来设置文件 与用户相关的权限。mode可用的权限如下表所示,表中各个参数可使用“| ”来组 合。

表文件权限

\标志位含义
当前用户S_IRUSR用户拥有读权限
\S_IWUSR用户拥有写权限
\S_IXUSR用户拥有执行权限
\S_IRWXU用户拥有读、写、执行权限
当前用户组S_IRGRP当前用户组的其他用户拥有读权限
\S_IWGRP当前用户组的其他用户拥有写权限
\S_IXGRP当前用户组的其他用户拥有执行权限
\S_IRWXG当前用户组的其他用户拥有读、写、执行权限
其他用户S_IROTH其他用户拥有读权限
\S_IWOTH其他用户拥有写权限
\S_IXOTH其他用户拥有执行权限
\S_IROTH其他用户拥有读、写、执行权限

read函数

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);

read函数用于从文件中读取若干个字节的数据,保存到数据缓冲区buf中,并返 回实际读取的字节数,具体函数参数如下:

  • fd:文件对应的文件描述符,可以通过fopen函数获得。另外,当一个程序 运行时,Linux默认有0、1、2这三个已经打开的文件描述符,分别对应了标准输入、标准输出、标准错误输出,即可以直接访问这三种文件描述符;
  • buf:指向数据缓冲区的指针;
  • count:读取多少个字节的数据。

write函数

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);

write函数用于往文件写入内容,并返回实际写入的字节长度,具体函数参数如下:

  • fd:文件对应的文件描述符,可以通过fopen函数获得。
  • buf:指向数据缓冲区的指针;
  • count:往文件中写入多少个字节。

close函数

int close(int fd);

当我们完成对文件的操作之后,想要关闭该文件,可以调用close函数,来关闭该fd文件描述符对应的文件。

lseek函数

lseek函数可以用与设置文件指针的位置,并返回文件指针相对于文件头 的位置。其函数原型如下:

off_t lseek(int fd, off_t offset, int whence);

它的用法与flseek一样,其中的offset参数用于指定位置,whence参数则定义了offset的意义,whence的可取值如下:

  • SEEK_SET:offset是一个绝对位置。
  • SEEK_END:offset是以文件尾为参考点的相对位置。
  • SEEK_CUR:offset是以当前位置为参考点的相对位置。

代码分析

#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>

//文件描述符
int fd;
char str[100];


int main(void)
{
   //创建一个文件
   fd = open("testscript.sh", O_RDWR|O_CREAT|O_TRUNC, S_IRWXU);
   //文件描述符fd为非负整数
   if(fd < 0){
      printf("Fail to Open File\n");
      return 0;
   }
   //写入字符串pwd
   write(fd, "pwd\n", strlen("pwd\n"));

   //写入字符串ls
   write(fd, "ls\n", strlen("ls\n"));

   //此时的文件指针位于文件的结尾处,使用lseek函数使文件指针回到文件头
   lseek(fd, 0, SEEK_SET);

   //从文件中读取100个字节的内容到str中,该函数会返回实际读到的字节数
   read(fd, str, 100);

   printf("File content:\n%s \n", str);

   close(fd);

   return 0;
}

执行流程

本实验与C库文件操作类似,也是创建文件、写入内容然后读出,不过此处使用 的都是系统调用函数如open、write、lseek、read、close,具体说明如下:

  • 代码中先调用了open函数以可读写的方式打开一个文本文件,并且O_CREAT指定 如果文件不存在,则创建一个新的文件,文件的权限为S_IRWXU,即当前用户可读 可写可执行,当前用户组和其他用户没有任何权限。
  • open与fopen的返回值功能类似,都是文件描述符,不过open使用非负整数 来表示正常,失败时返回-1,而fopen失败时返回NULL。
  • 创建文件后调用write函数写入了“pwdn”、“lsn”这样的字符串,实际上就是简单的Shell命令。
  • 使用read函数读取内容前,先调用lseek函数重置了文件指针至文件开头处读取。与C库文件 操作的区别write和read之间不需要使用fflush确保缓冲区的内容并写入,因为系统调用的文件操作是没有缓冲区的。
  • 最后关闭文件,释放文件描述符。

头文件目录

示例代码中的开头包含了一系列Linux系统常用的头文件。今后学习Linux的过程中,我们可能会接触各种 各样的头文件,因此了解一下Linux中头文件的用法十分有必要。

在linux中,大部分的头文件在系统的“/usr/include”目录下可以找到,它是 系统自带的GCC编译器默认的头文件目录,如下图所示,如果把该目录下的stdio.h文件删除 掉或更改名字(想尝试请备份),那么使用GCC编译hello world的程序会因为找不到stdio.h文件而报错。

代码中一些头文件前包含了某个目录,比如sys/stat.h,这些头文件 可以在编译器文件夹中的目录下找到。我们通常可以使用locate命令来搜索,如:

#在Ubuntu主机下执行如下命令:
locate sys/stat.h

如下图:

在上图中我们查找出sys/stat.h存在三个位置,这是因为示例的主机安装 了三个版本的编译器,每个编译器都有自己的头文件,编译器运行时默认是采用自己目录下的头文件编译的。

常用头文件

在后面的学习中我们常常会用到以下头文件,此处进行简单说明,若想查看具体的头文件内容,使用locate命令找到该文件目录后打开即可:

  • 头文件stdio.h:C标准输入与输出(standard input & output)头文件,我们经常使用的打印函数printf函数就位于该头文件中。
  • 头文件stdlib.h:C标准库(standard library)头文件,该文件包含了常用的malloc函数、free函数。
  • 头文件sys/stat.h:包含了关于文件权限定义,如S_IRWXU、S_IWUSR,以 及函数fstat用于查询文件状态。涉及系统调用文件相关的操作,通常都需要用到sys/stat.h文件。
  • 头文件unistd.h:UNIX C标准库头文件,unix,linux系列的操 作系统相关的C库,定义了unix类系统POSIX标准的符号常量头文件,比如Linux标准的输入文件描述符(STDIN),标准输出文件描述符(STDOUT),还有read、write等系统调用的声明。
  • 头文件fcntl.h:unix标准中通用的头文件,其中包含的相关函数有 open,fcntl,close等操作。
  • 头文件sys/types.h:包含了Unix/Linux系统的数据类型的头文件,常用的有size_t,time_t,pid_t等类型。

编译及测试

本实验使用的Makefile与上一篇文章的完全一样,不再分析。

对于ARM架构的程序,可使用如下步骤进行编译:

#在主机的实验代码Makefile目录下编译
#编译arm平台的程序
make ARCH=arm

编译后生成的ARM平台程序为build_arm/file_demo,使用网络文件系统共享 至开发板,在开发板的终端上测试即可。

如下图

如何决择

既然C标准库和系统调用都能够操作文件,那么应该选择哪种操作呢?考虑的因素如下:

  • 使用系统调用会影响系统的性能。执行系统调用时,Linux需要从用 户态切换至内核态,执行完毕再返回用户代码,所以减少系统调用能减 少这方面的开销。如库函数写入数据的文件操作fwrite最后也是执行了write系统 调用,如果是写少量数据的话,直接执行write可能会更高效,但如果是频繁的写入操作,由于f write的缓冲区可以减少调用write的次数,这种情况下使用fwrite能更节省时间。
  • 硬件本身会限制系统调用本身每次读写数据块的大小。如针对某种存 储设备的write函数每次可能必须写4kB的数据,那么当要写入的实际数据小于4kB时,write也只能 按4kB写入,浪费了部分空间,而带缓冲区的fwrite函数面对这种情况,会尽量在满足数据长度要求时才执行系统调用,减少空间开销。
  • 也正是由于库函数带缓冲区,使得我们无法清楚地知道它何时才会真正地把内容写入到硬件上,所以在需 要对硬件进行确定的控制时,我们更倾向于执行系统调用。

下面几篇文章,我将学习使用系统调用的文件操作方式操作arm开发板串口。