【嵌入式Linux应用开发基础】文件I/O基础编程
目录
一、文件I/O简介
二、文件描述符
2.1. 唯一性
2.2. 抽象性
2.3. 有限性
三、文件操作函数
四、标准文件I/O函数
五、文件执行权限
5.1. 权限类型
5.2. 权限分配对象
5.3. 权限表示方法
5.4. 权限设置命令
5.5. 权限设置的重要性
5.6. 实例说明
六、设备文件读写
6.1. 设备文件类型
6.2. 设备文件的命名
6.3. 设备文件的读写操作
6.4. 示例:串口通信
在嵌入式 Linux 应用开发中,文件 I/O(Input/Output)基础编程是非常重要的一部分,它允许程序与文件系统进行交互,实现数据的读取、写入和管理等操作。
一、文件I/O简介
Linux文件I/O是操作系统中处理文件读写操作的基本机制。在Linux系统中,文件I/O操作是通过系统调用实现的,这些系统调用允许用户空间的程序与内核空间的文件系统进行交互。一个通用的IO模型通常包括打开文件、读写文件、关闭文件这些基本操作。
- 文件描述符(File Descriptor):在 Linux 系统中,每个打开的文件都由一个非负整数的文件描述符来标识。当程序打开一个现有文件或者创建一个新文件时,内核会返回一个文件描述符。标准输入(stdin)、标准输出(stdout)和标准错误输出(stderr)的文件描述符分别是 0、1 和 2。
- 缓冲区(Buffer):为了提高文件 I/O 的效率,系统通常会使用缓冲区。缓冲区是一块内存区域,用于临时存储从文件读取或要写入文件的数据。
二、文件描述符
文件描述符(File Descriptor)是Linux和UNIX系统编程中的一个重要概念,它是一个用于标识打开文件或其他输入/输出资源的非负整数。文件描述符允许程序通过一个抽象的数字来引用文件和其他输入输出资源,而不是直接使用文件名或设备名。
2.1. 唯一性
在进程的生命周期内,每个打开的文件或设备都会分配一个唯一的文件描述符。这些描述符是从3开始分配的,因为0、1、2已经被系统预留给标准输入(stdin)、标准输出(stdout)和标准错误(stderr)了。
例如,如果进程首先打开一个文件,它将被分配文件描述符3;接着打开第二个文件,则分配文件描述符4,以此类推。
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd1 = open("file1.txt", O_RDONLY);
int fd2 = open("file2.txt", O_RDONLY);
int fd3 = open("file3.txt", O_RDONLY);
if (fd1 == -1 || fd2 == -1 || fd3 == -1) {
perror("open");
return 1;
}
printf("File descriptors: file1.txt = %d, file2.txt = %d, file3.txt = %d\n", fd1, fd2, fd3);
close(fd1);
close(fd2);
close(fd3);
return 0;
}
我们打开了三个文件,并打印了它们的文件描述符。通过运行,可以观察到文件描述符是从3开始递增分配的(假设0、1、2没有被占用或重定向)。
2.2. 抽象性
文件描述符提供了一种抽象机制,使得程序可以通过简单的数字来引用复杂的I/O资源。这种抽象性简化了编程模型,因为程序员不需要关心底层的设备或文件实现细节。
文件描述符的这种抽象性也支持了重定向和管道等高级I/O操作。例如,可以将一个进程的标准输出重定向到一个文件,或者将一个进程的输出作为另一个进程的输入,这些操作都可以通过操作文件描述符来实现。
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
int fd = open("example.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open");
return 1;
}
const char *text = "Hello, file descriptor!\n";
ssize_t bytes_written = write(fd, text, strlen(text));
if (bytes_written == -1) {
perror("write");
close(fd);
return 1;
}
close(fd);
return 0;
}
打开(或创建)了一个文件,并使用write
函数通过文件描述符向其中写入数据。文件描述符在这里作为I/O操作的抽象引用。
2.3. 有限性
文件描述符的数量是有限的,这个限制通常由系统设置决定。在Linux系统中,可以使用ulimit -n
命令来查看和设置当前shell进程的文件描述符限制。
默认情况下,这个限制可能比较低(如1024),但在现代系统中,这个限制通常可以被提高。提高文件描述符限制对于需要打开大量文件的服务器程序来说是非常重要的。
需要注意的是,虽然系统允许提高文件描述符限制,但这也受到系统资源(如内存)的限制。打开过多的文件可能会导致系统资源耗尽,从而影响系统的稳定性和性能。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/resource.h>
int main() {
struct rlimit rl;
// 获取当前文件描述符限制
if (getrlimit(RLIMIT_NOFILE, &rl) == -1) {
perror("getrlimit");
exit(EXIT_FAILURE);
}
printf("Current file descriptor limit: soft = %lld, hard = %lld\n",
(long long)rl.rlim_cur, (long long)rl.rlim_max);
// 尝试提高软限制(在硬限制范围内)
rl.rlim_cur = rl.rlim_max; // 或者设置为一个较小的值,但不超过硬限制
if (setrlimit(RLIMIT_NOFILE, &rl) == -1) {
perror("setrlimit");
exit(EXIT_FAILURE);
}
// 再次获取限制以确认更改
if (getrlimit(RLIMIT_NOFILE, &rl) == -1) {
perror("getrlimit");
exit(EXIT_FAILURE);
}
printf("New file descriptor limit: soft = %lld, hard = %lld\n",
(long long)rl.rlim_cur, (long long)rl.rlim_max);
return 0;
}
首先获取了当前的文件描述符限制(软限制和硬限制),然后尝试将软限制提高到硬限制的值。请注意,硬限制是由系统管理员设置的,普通用户可能无法更改它。如果尝试设置一个超过硬限制的值,setrlimit
调用将失败。
三、文件操作函数
在Linux系统中,文件操作主要涉及到以下几个函数:
- open()函数:用于打开文件。其原型为
int open(const char *pathname, int flags, mode_t mode)
。其中,pathname
是文件名或路径,flags
用于指定文件的打开模式(如只读、只写、读写等),mode
用于设置文件权限(当创建新文件时)。 - read()函数:用于从文件中读取数据。其原型为
ssize_t read(int fd, void *buf, size_t count)
。其中,fd
是文件描述符,buf
是指向存储读取数据的缓冲区的指针,count
是要读取的字节数。 - write()函数:用于向文件中写入数据。其原型为
ssize_t write(int fd, const void *buf, size_t count)
。参数含义与read()
函数类似。 - close()函数:用于关闭文件。其原型为
int close(int fd)
。其中,fd
是文件描述符。 - lseek()函数:用于移动文件指针。其原型为
off_t lseek(int fd, off_t offset, int whence)
。其中,fd
是文件描述符,offset
是偏移量,whence
用于指定偏移的基准位置(如文件开头、当前位置、文件末尾等)。 - creat()函数:用于创建文件。其原型为
int creat(const char *pathname, mode_t mode)
。其中,pathname
是文件名或路径,mode
用于设置文件权限。不过,在现代Linux系统中,creat()
函数已经被open()
函数所取代,因为open()
函数提供了更丰富的功能。
四、标准文件I/O函数
除了上述低级的文件操作函数外,Linux还提供了一套标准的文件I/O函数,这些函数封装了复杂的底层细节,便于用户进行日常文件操作。标准文件I/O函数主要包括:
- fopen()函数:用于打开文件。其原型为
FILE *fopen(const char *filename, const char *mode)
。 - fclose()函数:用于关闭文件。其原型为
int fclose(FILE *stream)
。 - fread()函数:用于从文件中读取数据。其原型为
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
。 - fwrite()函数:用于向文件中写入数据。其原型为
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
。 - fgets()函数:用于从文件中读取一行字符。其原型为
char *fgets(char *str, int n, FILE *stream)
。 - fputs()函数:用于向文件中写入字符串。其原型为
int fputs(const char *str, FILE *stream)
。
五、文件执行权限
在Linux系统中,文件执行权限是控制用户可以对文件执行哪些操作的重要机制。正确设置文件权限对于系统的安全性至关重要
5.1. 权限类型
Linux文件权限主要分为三类:
- 读权限(r):允许用户读取文件内容或列出目录内容。
- 写权限(w):允许用户修改文件内容或在目录中创建、删除文件。
- 执行权限(x):允许用户执行文件(如果文件是可执行文件)或进入目录。
5.2. 权限分配对象
这些权限可以被分配给以下三个对象:
- 文件所有者(owner):文件的创建者或拥有者,对文件具有最高的控制权限。
- 文件所属组(group):文件所属的用户组,组内的所有用户共享这些权限。
- 其他用户(others):既不是文件所有者,也不属于文件所属组的用户。
5.3. 权限表示方法
Linux系统提供两种表示文件权限的方法:数字表示法和符号表示法。
-
数字表示法:
- 读权限(r)= 4
- 写权限(w)= 2
- 执行权限(x)= 1
- 将这三种权限的数字相加,就可以得到每个用户类别的权限值。例如,7表示读、写和执行权限(4+2+1),5表示读和执行权限(4+1)。
-
符号表示法:使用字符来表示权限,通常与用户名、组名一起显示在
ls -l
命令的输出中。例如,-rwxr-xr--
表示一个普通文件,所有者有读、写和执行权限,组用户有读和执行权限,其他用户只有读权限。
5.4. 权限设置命令
在Linux中,可以使用chmod
命令来设置或修改文件权限。
-
符号表示法设置权限:
chmod u+x 文件名
:给文件的所有者添加执行权限。chmod g+w,o+r 文件名
:给用户组增加写权限,给其他用户增加读权限。chmod a=r 文件名
:将文件的权限设置为所有人仅具有读权限。
-
数字表示法设置权限:
chmod 755 文件名
:设置文件所有者为读写执行权限(7),用户组和其他用户为读执行权限(5)。chmod 644 文件名
:设置文件所有者为读写权限(6),用户组和其他用户为读权限(4)。
5.5. 权限设置的重要性
正确设置文件权限对于Linux系统的安全性至关重要。通过合理设置文件权限,可以控制不同用户对文件和目录的访问和操作,防止未经授权的访问和修改,从而保护系统资源的安全。
5.6. 实例说明
假设有一个名为script.sh
的Shell脚本文件,需要给其所有者添加执行权限,以便能够执行该脚本。可以使用以下命令:
chmod u+x script.sh
或者,也可以使用数字表示法来设置权限:
chmod 755 script.sh
这样,script.sh
文件的所有者将拥有读、写和执行权限,而用户组和其他用户将拥有读和执行权限(虽然对于脚本文件来说,写权限通常不是必需的,但这里为了演示目的而包含)。
六、设备文件读写
在嵌入式Linux系统中,设备文件是一种将硬件设备抽象为普通文件的机制。这种抽象使得用户空间程序可以通过标准的文件I/O操作(如open
、read
、write
、close
等)来与硬件设备进行交互。设备文件通常位于/dev
目录下,并且根据其特性被分类为字符设备或块设备。
6.1. 设备文件类型
- 字符设备:字符设备以字符为单位进行数据传输,如串口(UART)、键盘、鼠标等。对字符设备的读写操作通常不会涉及缓存,因为数据是即时处理的。
- 块设备:块设备以块(通常是512字节或更大)为单位进行数据传输,如硬盘、SD卡等。对块设备的读写操作可能会涉及缓存,以提高性能。
6.2. 设备文件的命名
- 设备文件通常以设备类型加上设备编号的形式命名。例如,
/dev/ttyS0
可能表示第一个串口设备,而/dev/sda1
可能表示第一个SCSI硬盘的第一个分区。
6.3. 设备文件的读写操作
在嵌入式编程中,对设备文件的读写操作通常涉及以下步骤。
- 打开设备文件:使用
open
函数打开设备文件,指定操作模式(如读、写或读写)。 - 配置设备(如果需要):对于某些设备,可能需要通过
ioctl
函数发送控制命令来配置设备参数。 - 读写操作:
- 使用
read
函数从设备读取数据。 - 使用
write
函数向设备写入数据。
- 使用
- 处理错误:检查每个系统调用的返回值,以处理可能的错误情况。
- 关闭设备文件:使用
close
函数关闭设备文件,释放资源。
6.4. 示例:串口通信
以下是一个简单的示例,展示如何通过读写串口设备文件来进行通信:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
int main() {
int fd;
struct termios options;
// 打开串口设备文件
fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY | O_SYNC);
if (fd < 0) {
perror("open");
exit(EXIT_FAILURE);
}
// 配置串口参数
tcgetattr(fd, &options);
cfsetispeed(&options, B9600); // 设置输入波特率
cfsetospeed(&options, B9600); // 设置输出波特率
options.c_cflag |= (CLOCAL | CREAD); // 启用接收器,忽略调制解调器控制线
options.c_cflag &= ~PARENB; // 无奇偶校验
options.c_cflag &= ~CSTOPB; // 一个停止位
options.c_cflag &= ~CSIZE;
options.c_cflag |= CS8; // 8个数据位
tcsetattr(fd, TCSANOW, &options);
// 写入数据到串口
const char *msg = "Hello, UART!\n";
write(fd, msg, strlen(msg));
// 从串口读取数据(这里只是示例,实际应用中可能需要循环读取)
char buf[256];
int n = read(fd, buf, sizeof(buf) - 1);
if (n > 0) {
buf[n] = '\0'; // 确保字符串以null结尾
printf("Received: %s", buf);
} else if (n < 0) {
perror("read");
}
// 关闭串口设备文件
close(fd);
return 0;
}
首先打开了/dev/ttyS0
设备文件,配置了串口参数(如波特率、数据位、停止位等),然后向串口写入了数据,并从串口读取了数据(虽然在实际应用中,读取操作通常是在一个循环中进行的)。最后,关闭了设备文件。
综上所述,嵌入式Linux应用开发中的文件I/O基础编程涉及到文件描述符、文件操作函数、标准文件I/O函数以及文件执行权限等多个方面。掌握这些基础知识对于进行嵌入式Linux应用开发至关重要。