news 2026/6/21 18:07:35

Linux C 应用编程 学习Day1-2(文件IO基础)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux C 应用编程 学习Day1-2(文件IO基础)

一切从头开始,打牢基础

1. 应用编程概念

首先插入一张内核系统调用与应用程序的关系图,进一步探讨应用编程与裸机编程、驱动编程有什么区别?

裸机编程:一般把没有操作系统支持的编程环境称为裸机编程环境,譬如单片机上的编程开发,编写直接在硬件上运行的程序,没有操作系统支持。

Linux 驱动编程:指的是基于内核驱动框架开发驱动程序,驱动开发工程师通过调用 Linux 内核提供的接口完成设备驱动的注册,驱动程序负责底层硬件操作相关逻辑。

Linux 应用编程(系统编程):则指的是基于 Linux 操作系统的应用编程,在应用程序中通过调用系统调用 API 完成应用程序的功能和逻辑,应用程序运行于操作系统之上。

操作系统下有两种不同的状态:内核态和用户态,应用程序运行在用户态、而内核则运行在内核态。
同时注意,驱动程序属于内核的一部分,当操作系统启动的时候会加载驱动程序。
有操作系统支持的情况下,应用程序处于用户态,而驱动程序处于内核态。

1.库函数是属于应用层,而系统调用是内核提供给应用层的编程接口,属于系统内核的一部分;
2.库函数运行在用户空间,调用系统调用会由用户空间(用户态)陷入到内核空间(内核态);
3.库函数通常是有缓存的,而系统调用是无缓存的,所以在性能、效率上,库函数通常要优于系统调用;
4.可移植性:库函数相比于系统调用具有更好的可移植性,通常对于不同的操作系统,其内核向应用层提供的系统调用往往都是不同,譬如系统调用的定义、功能、参数列表、返回值等往往都是不一样的;而对于 C 语言库函数来说,由于很多操作系统都实现了 C 语言库, C 语言库在不同的操作系统之间其接口定义几乎是一样的,所以库函数在不同操作系统之间相比于系统调用具有更好的可移植性。

二、文件IO基础

2.1 IO基础了解

文件 I/O 指的是对文件的输入/输出操作,说白了就是对文件的读写操作;

Linux 下一切皆文件,文件作为 Linux 系统设计思想的核心理念,在 Linux 系统下显得尤为重要,所以对文件的 I/O 操作作既是基础也是最重要的部分。

一个通用的 IO 模型通常包括打开文件、读写文件、关闭文件这些基本操作,主要涉及到 4 个函数: open()、 read()、 write()以及 close()。

#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> int main(void) { char buff[1024]; int fd1, fd2; int ret; /* 打开源文件 src_file(只读方式) */ fd1 = open("./src_file", O_RDONLY); if (-1 == fd1) return fd1; /* 打开目标文件 dest_file(只写方式) */ fd2 = open("./dest_file", O_WRONLY); if (-1 == fd2) { ret = fd2; goto out1; } /* 读取源文件 1KB 数据到 buff 中 */ ret = read(fd1, buff, sizeof(buff)); if (-1 == ret) goto out2; /* 将 buff 中的数据写入目标文件 */ ret = write(fd2, buff, sizeof(buff)); if (-1 == ret) goto out2; ret = 0; out2: /* 关闭目标文件 */ close(fd2); out1: /* 关闭源文件 */ close(fd1); return ret; }

从源文件 src_file 中读取 1KB 数据,然后将其写入到目标文件 dest_file 中

2.2 文件描述符

调用 open 函数会有一个返回值,在 open 函数执行成功的情况下,会返回一个非负整数,该返回值就是一个文件描述符(file descriptor)。

对于 Linux 内核而言,所有打开的文件都会通过文件描述符进行索引

文件描述符就是 Linux 给打开的文件发的专属门牌号,所有文件操作都靠这个号来定位,系统还限制了每个进程最多能拿多少个门牌号,防止占满内存。

三个默认的「标准文件描述符」

每个进程一启动,系统就自动给它开了 3 个文件,对应 3 个固定编号:

  • 0:标准输入(stdin)→ 对应键盘,你用scanf()读输入,就是用这个编号
  • 1:标准输出(stdout)→ 对应屏幕,你用printf()打印内容,就是用这个编号
  • 2:标准错误(stderr)→ 对应屏幕,专门用来输出错误信息

2.3 open 打开文件

在 Linux 系统中要操作一个文件,需要先打开该文件,得到文件描述符,然后再对文件进行相应的读写操作(或其他操作),最后在关闭该文件; 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 系统下,可以通过 man 命令(也叫 man 手册)来查看某一个 Linux 系统调用的帮助信息,相当于帮助手册。

注意:man 命令后面跟着两个参数,数字 2 表示系统调用, man 命令除了可以查看系统调用的帮助信息外,还可以查看 Linux 命令(对应数字 1)以及标准 C 库函数(对应数字 3)所对应的帮助信息;最后一个参数 open 表示需要查看的系统调用函数名。

从图中可知,在应用程序中使用 open 函数时,需要包含 3 个头文件“ #include <sys/types.h>”、“#include <sys/stat.h>”、“#include <fcntl.h>”。

flags 参数可以用位或运算将多个标志进行组合

mode:此参数用于指定新建文件的访问权限,只有当 flags 参数中包含 O_CREAT 或O_TMPFILE标志时才有效(O_TMPFILE 标志用于创建一个临时文件)。权限对于文件来说是一个很重要的属性,那么在 Linux系统中,我们可以通过touch 命令新建一个文件,此时文件会有一个默认的权限,如果需要修改文件权限,可通过 chmod 命令对文件权限进行修改,譬如在 Linux 系统下我们可以使用"ls -l"命令来查看到文件所对应的权限。

当我们调用 open 函数去新建一个文件时,也需要指定该文件的权限,而 mode 参数便用于指定此文件的权限,接下来看看我们该如何通过 mode 参数来表示文件的权限,首先 mode 参数的类型是 mode_t,这是一个 u32 无符号整形数据,权限表示方法如下所示:

关于什么是文件所属用户、同组用户以及其他用户,这些都是 Linux 操作系统相关的基础知识,大家都理解这些概念; 3 个 bit 位中,按照 rwx 顺序来分配权限位(特殊权限除外),最高位(权值为 4)表示读权限,为 1 时表示具有读权限,为 0 时没有读权限;中间位(权值为 2)表示写权限,为 1 时表示具有写权限,为 0 时没有写权限;最低位(权值为 1)表示执行权限,为 1 时表示具有可执行权限,为 0 时没有执行权限。

最高权限表示方法: 111111111(二进制表示)、 777(八进制表示)、 511(十进制表示);
最高权限这里意味着所有用户对此文件都具有读权限、写权限以及执行权限。

111000000(二进制表示):表示文件所属者具有读、写、执行权限,而同组用户其他用户不具有任何权限;
100100100(二进制表示):表示文件所属者、同组用户以及其他用户都具有读权限,但都没有写、执行权限

Tips: open 函数 O_RDONLY、 O_WRONLY 以及 O_RDWR 这三个标志表示以什么方式去打开文件


(3) 使用 open 函数打开一个指定的文件(譬如/home/dengtao/hello),使用可读可写方式,如果该文件是一个符号链接文件,则不对其进行解引用,直接返回错误:

2.4 write 写文件

调用 write 函数可向打开的文件写入数据,其函数原型如下所示(可通过"man 2 write"查看):

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

首先使用 write 函数需要先包含 unistd.h 头文件
函数参数和返回值含义如下:
fd:文件描述符。关于文件描述符,前面已经给大家进行了简单地讲解,这里不再重述!我们需要将进行写操作的文件所对应的文件描述符传递给 write 函数。

buf:指定写入数据对应的缓冲区。

count:指定写入的字节数

返回值:如果成功将返回写入的字节数(0 表示未写入任何字节),如果此数字小于 count 参数,这不是错误,譬如磁盘空间已满,可能会发生这种情况;如果写入出错,则返回-1。

不管读还是写,都必须知道:从文件哪个地方开始?这个 “开始位置” 就叫位置偏移量。默认从 0 开始(文件开头)。你读多少、写多少,位置就自动往后挪多少。比如现在在 1000,读写 500 字节,位置就变成 1500。

读写从当前位置开始 → 操作完自动往后挪 → 位置 = 旧位置 + 读写的字节数

2.5 read 读文件

调用 read 函数可从打开的文件中读取数据,其函数原型如下所示(可通过"man 2 read"查看):

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

首先使用 read 函数需要先包含 unistd.h 头文件
函数参数和返回值含义如下:

fd:文件描述符。与 write 函数的 fd 参数意义相同。

buf:指定用于存储读取数据的缓冲区。

count:指定需要读取的数字

返回值:如果读取成功将返回读取到的字节数,实际读取到的字节数可能会小于 count 参数指定的字节数,也有可能会为 0,譬如进行读操作时,当前文件位置偏移量已经到了文件末尾。实际读取到的字节数少于要求读取的字节数,譬如在到达文件末尾之前有 30 个字节数据,而要求读取 100 个字节,则 read 读取成功只能返回 30;而下一次再调用 read 读,它将返回 0(文件末尾)


注意:补充一点指针知识理解

*有两个作用 解指针 和 声明指针

*在这里不是 “取内容”,而是用来声明「指针类型」,表示这个参数是一个「内存地址」,而不是一个普通变量。

read要做的事:

从文件里读数据,把读到的字节,放到你指定的一块内存里

那怎么告诉系统 “放到哪块内存”?

  • 不能直接传一块内存本身(比如一个数组),C 语言里函数传参只能传「值」
  • 只能传这块内存的起始地址,系统拿到地址,就知道往哪写数据了

所以buf参数的类型必须是指针,也就是void *buf

  • void *是「通用指针类型」,可以接收任意类型的地址(比如char[]int[]的首地址)
  • buf是指针变量,存的是你给的缓冲区的内存地址
char buf[1024]; buf = 地址(数组开头) *buf = 内容(buf[0]) &buf[0] = 地址(和 buf 完全一样) read(fd, buf, 100); # 这是正确写法 buf 是数组名,代表数组首元素的地址,类型是 char*

ssize_t read(int fd, void *buf, size_t count);这句话中有void *buf

void *buf🔥🔥🔥(最晕的地方)

void *buf

我用最简单到不能再简单的话讲:

意思:我需要一块内存的地址!

  • *→ 代表这是指针(地址)
  • buf→ 这块内存的名字(缓冲区)
  • void *万能地址类型,能接收任何内存地址

所以:void *buf=给我一个地址,我要把数据放到这里

2.6 close 关闭文件

可调用 close 函数关闭一个已经打开的文件,其函数原型如下所示(可通过"man 2 close"查看)

#include <unistd.h> int close(int fd);

首先使用 close 函数需要先包含 unistd.h 头文件,当我们对文件进行 IO 操作完成之后,后续不再对文件进行操作时,需要将文件关闭。

函数参数和返回值含义如下:
fd:
文件描述符,需要关闭的文件所对应的文件描述符。
返回值:如果成功返回 0,如果失败则返回-1
除了使用 close 函数显式关闭文件之外,在 Linux 系统中,当一个进程终止时,内核会自动关闭它打开的所有文件,也就是说在我们的程序中打开了文件,如果程序终止退出时没有关闭打开的文件,那么内核会自动将程序中打开的文件关闭。很多程序都利用了这一功能而不显式地用 close 关闭打开的文件。

显式关闭不再需要的文件描述符往往是良好的编程习惯,会使代码在后续修改时更具有可读性,也更可靠,进而言之,文件描述符是有限资源,当不再需要时必须将其释放、归还于系统。

2.7 lseek

对于每个打开的文件,系统都会记录它的读写位置偏移量,我们也把这个读写位置偏移量称为读写偏移量,记录了文件当前的读写位置,当调用 read()或 write()函数对文件进行读写操作时,就会从当前读写位置偏移量开始进行数据读写。

读写偏移量用于指示 read()或 write()函数操作时文件的起始位置,会以相对于文件头部的位置偏移量来表示,文件第一个字节数据的位置偏移量为 0。

当打开文件时,会将读写偏移量设置为指向文件开始位置处,以后每次调用 read()、 write()将自动对其进行调整,以指向已读或已写数据后的下一字节,因此,连续的调用 read()和 write()函数将使得读写按顺序递增,对文件进行操作。我们先来看看 lseek 函数的原型,如下所示(可通过"man 2 lseek"查看)。

#include <sys/types.h> #include <unistd.h> off_t lseek(int fd, off_t offset, int whence);

首先调用 lseek 函数需要包含<sys/types.h>和<unistd.h>两个头文件。

函数参数和返回值含义如下
fd: 文件描述符。
offset: 偏移量,以字节为单位。
whence: 用于定义参数 offset 偏移量对应的参考值,该参数为下列其中一种(宏定义):
SEEK_SET:读写偏移量将指向 offset 字节位置处(从文件头部开始算);

SEEK_CUR:读写偏移量将指向当前位置偏移量 + offset 字节位置处, offset 可以为正、也可以为负,如果是正数表示往后偏移,如果是负数则表示往前偏移;

SEEK_END:读写偏移量将指向文件末尾 + offset 字节位置处,同样 offset 可以为正、也可以为负,如果是正数表示往后偏移、如果是负数则表示往前偏移。

返回值:成功将返回从文件头部开始算起的位置偏移量(字节为单位),也就是当前的读写位置;发生错误将返回-1。

(1)将读写位置移动到文件开头处:

off_t off = lseek(fd, 0, SEEK_SET); if (-1 == off) return -1;

(2)将读写位置移动到文件末尾:

off_t off = lseek(fd, 0, SEEK_END); if (-1 == off) return -1;

(3)将读写位置移动到偏移文件开头 100 个字节处:

off_t off = lseek(fd, 100, SEEK_SET); if (-1 == off) return -1;

(4)获取当前读写位置偏移量:

off_t off = lseek(fd, 0, SEEK_CUR); if (-1 == off) return -1;

函数执行成功将返回文件当前读写位置。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/13 21:15:20

大数据分析:核心概念 + 在网络领域的全方位应用(超清晰易懂版)

大数据分析&#xff1a;核心概念 在网络领域的全方位应用&#xff08;超清晰易懂版&#xff09;前言一、什么是大数据分析&#xff1f;1. 定义2. 通俗理解二、大数据分析的 4 个关键特点三、大数据分析在网络中的核心应用&#xff08;重点、必背&#xff09;1. 网络流量智能分…

作者头像 李华
网站建设 2026/4/13 21:05:57

PCN-224/AuNPs,PCN-224@金纳米颗粒复合材料,合成及纯化过程

PCN-224/AuNPs&#xff0c;PCN-224金纳米颗粒复合材料&#xff0c;合成及纯化过程PCN-224/AuNPs&#xff08;PCN-224金纳米颗粒复合材料&#xff09;**是一类以锆基金属有机骨架PCN-224为载体、负载金纳米颗粒&#xff08;AuNPs&#xff09;构建的复合纳米材料。该体系通常通过…

作者头像 李华