跳到主要内容

系统IO

1. 基本概念

Linux系统的IO指的是输入/输出(Input/Output)。在Linux中,所有输入和输出都通过文件来完成,因此,Linux系统的IO可以被看作是对文件进行操作。

在Linux系统中,程序通过文件描述符来访问文件,其中,文件描述符是一个整数,用于标识一个文件。每个文件都有一个唯一的文件描述符,程序可以通过文件描述符来读取或写入文件。

在Linux系统中,有三种基本的IO操作:读操作、写操作和错误操作。读操作用于从文件中读取数据,写操作用于向文件中写入数据,错误操作用于检查文件是否发生错误。

在Linux系统中,还可以通过文件状态标志来控制IO操作。文件状态标志是一个整数,用于设置文件的读写模式,如是否允许读操作、是否允许写操作等。

总之,Linux系统的IO操作是通过文件描述符和文件状态标志来完成的。程序可以通过这两个机制来控制文件的读写操作,实现输入输出功能。

2. 打开文件

open()函数用于在 Linux 系统中打开文件。函数原型为:

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

该函数接受三个参数:

  • pathname:指向要打开的文件路径的字符串。
  • flags:指定文件的打开模式,如读、写、读写等。
  • mode:指定文件的权限,如用户权限、组权限等。

如果打开文件成功,则该函数返回文件描述符。否则,返回 -1。

flags参数指定了文件的打开模式,具体可以使用的值有:

  • O_RDONLY:只读模式,只允许读取文件内容。
  • O_WRONLY:只写模式,只允许写入文件内容。
  • O_RDWR:读写模式,既可以读取文件内容,也可以写入文件内容。
  • O_APPEND:追加模式,每次写入操作都会将数据追加到文件末尾。
  • O_CREAT:如果文件不存在,则创建。
  • O_EXCL:如果使用 O_CREAT 选项且文件存在,则创建该文件。
  • O_TRUNC:表示如果文件存在,则清空文件内容。
  • O_NOCTTY:如果文件为终端,那么终端不可以作为调用 open( ) 系统调用的那个进程的控制终端。

例如,下面的代码演示了如何使用open()函数打开一个文件:

// 打开文件
int fd = open("test.txt", O_RDWR, 0644);
if (fd == -1) {
// 打开文件失败
perror("open() fail");
exit(EXIT_FAILURE);
}

3. 关闭文件

close()函数用于在 Linux 系统中关闭文件。函数原型为:

int close(int fd);

该函数接受一个参数:

  • fd:指定要关闭的文件的文件描述符。

如果关闭文件成功,则该函数返回 0。否则,返回 -1。

注意:重复关闭一个已经关闭了的文件或者尚未打开的文件是安全的。

4. 写入文件

write()函数用于在 Linux 系统中将数据写入文件。函数原型为:

ssize_t write(int fd, const void *buf, size_t count);

该函数接受三个参数:

  • fd:指定要写入的文件的文件描述符。
  • buf:指向要写入文件的数据缓冲区的指针。
  • count:指定要写入的数据的长度(单位:字节),实际写入的字节小于等于 count。

如果写入文件成功,则该函数返回实际写入的字节数。如果写入文件时出错,则返回 -1。

注意:在调用 write() 函数之前,需要使用 open() 函数打开文件。当不再需要对文件进行操作时,需要使用 close() 函数关闭文件。

5. 读取文件

read()函数用于在 Linux 系统中从文件中读取数据。函数原型为:

ssize_t read(int fd, void *buf, size_t count);

该函数接受三个参数:

  • fd:指定要读取的文件的文件描述符。
  • buf:指向用于存储读取数据的缓冲区的指针。
  • count:指定要读取的数据的长度(单位:字节),实际读取的字节小于等于 count。

如果读取文件成功,则该函数返回实际读取的字节数。如果读取文件时出错,则返回 -1。如果已到达文件末尾,则返回 0。

注意:在调用 read() 函数之前,需要使用 open() 函数打开文件。当不再需要对文件进行操作时,需要使用 close() 函数关闭文件。

6. 调整文件位置

lseek()函数用于在 Linux 系统中将文件指针移到指定位置。函数原型为:

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

该函数接受三个参数:

  • fd:指定要操作的文件的文件描述符。

  • offset:指定文件指针要移到的位置(单位:字节)。

  • whence:指定文件指针要移到的位置相对于哪个位置。可能的值有:

    • SEEK_SET:文件开头

    • SEEK_CUR:当前位置

    • SEEK_END:文件末尾

如果成功移动文件指针,则该函数返回文件指针移到的位置。如果文件指针移动失败,则返回 -1。

例如,以下代码将文件指针移到文件开头:

lseek(fd, 0, SEEK_SET);

以下代码将文件指针移到当前位置的下一个字节:

lseek(fd, 1, SEEK_CUR);

以下代码将文件指针移到文件末尾的前一个字节:

lseek(fd, -1, SEEK_END);

注意:lseek() 函数并不会修改文件的大小,它只是移动了文件指针。

7. 示例代码

#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#define BUF_SIZE 1024

int main(int argc, char *argv[])
{
// 打开文件
int fd = open("hello.txt", O_RDWR | O_CREAT, 0644);
if (fd == -1) {
// 文件打开失败
perror("open() fail");
exit(EXIT_FAILURE);
}

// 向文件中写入数据
char *msg = "Hello, world!\n";
size_t len = strlen(msg);
if (write(fd, msg, len) != len) {
// 写入文件失败
perror("write() fail");
exit(EXIT_FAILURE);
}
// 将文件指针移到文件开头
if (lseek(fd, 0, SEEK_SET) == -1) {
// 文件指针移动失败
perror("lseek() fail");
exit(EXIT_FAILURE);
}

// 循环读取文件中的数据
char buf[BUF_SIZE];
ssize_t numRead;
while ((numRead = read(fd, buf, BUF_SIZE)) > 0) {
// 将读取的数据写入标准输出
if (write(STDOUT_FILENO, buf, numRead) != numRead){
// 写入标准输出失败
perror("write() fail");
exit(EXIT_FAILURE);
}
}

if (numRead == -1) {
// 读取文件失败
perror("read() fail");
exit(EXIT_FAILURE);
}

// 关闭文件
int ret = close(fd);
if (ret == -1){
// 文件关闭失败
perror("close() fail");
exit(EXIT_FAILURE);
}

return 0;
}

8. 其他类型系统IO

8.1 复制文件描述符

复制文件描述符的一个常见用途是在多进程程序中共享文件。例如,父进程可以打开一个文件,然后通过 dup2() 函数复制文件描述符,并通过 fork() 函数创建子进程。这样,子进程就可以通过复制的文件描述符访问父进程中打开的文件,并与父进程共享文件。

dup()函数用于在 Linux 系统中复制一个文件描述符。函数原型为:

int dup(int oldfd);

该函数接受一个参数:

  • oldfd:指定要复制的文件描述符。

如果成功复制了文件描述符,则该函数返回新的文件描述符。如果复制文件描述符失败,则返回 -1。

例如,以下代码复制了文件描述符 fd

int newfd = dup(fd);

复制文件描述符后,新的文件描述符与原来的文件描述符指向同一个文件,并且文件描述符的当前位置也相同。但是,新的文件描述符可以被独立地操作,而不会影响原来的文件描述符。

注意:复制完文件描述符后,程序应该调用 close() 函数关闭复制的文件描述符以释放系统资源。

dup2()函数用于在 Linux 系统中复制一个文件描述符。函数原型为:

int dup2(int oldfd, int newfd);

该函数接受两个参数:

  • oldfd:指定要复制的文件描述符。
  • newfd:指定新的文件描述符,如果 newfd 已经打开,则它会被关闭。

如果成功复制了文件描述符,则该函数返回新的文件描述符。如果复制文件描述符失败,则返回 -1。

例如,以下代码复制了文件描述符 fd,并将新的文件描述符指定为 10:

int newfd = dup2(fd, 10);

复制文件描述符后,新的文件描述符与原来的文件描述符指向同一个文件,并且文件描述符的当前位置也相同。但是,新的文件描述符可以被独立地操作,而不会影响原来的文件描述符。

dup() 函数不同,dup2() 函数允许指定新的文件描述符。这可以在一些情况下提供更多的灵活性。例如,可以通过复制文件描述符来将一个已打开的文件重定向到另一个文件。

8.2 文件控制

ioctl()函数用于在 Linux 系统中控制设备。函数原型为:

int ioctl(int d, int request, ...);

该函数接受三个参数:

  • d:指定要控制的设备的文件描述符。
  • request:指定要执行的操作。
  • ...:其他可选参数,用于提供更多信息,根据执行的操作而不同。

如果成功控制设备,则该函数返回 0。如果控制设备失败,则返回 -1。

ioctl() 函数可以执行各种不同的操作,根据所指定的操作类型而不同。例如,可以通过 ioctl() 函数控制终端设备的大小、颜色和显示模式,或者控制磁盘驱动器的状态和性能。

为了执行指定的操作,必须向 ioctl() 函数提供一个操作类型,这通常是一个由宏定义的整数值。每个操作类型对应一种特定的操作,并且需要提供一定的参数来指定操作的细节。其 request 是一个由底层驱动提供的命令字,这些通用的命令字被放置在头文件 /usr/include/asm-generi/ioctls.h (不同的系统存放位置也许不同) 中,后面的变参也由前面的request 命令字决定。例如,可以使用 TIOCGWINSZ 常量作为操作类型,来查询终端设备的窗口大小:

#include <sys/ioctl.h>
#include <stdio.h>

struct winsize ws;

int main(int argc, char *argv[])
{
ioctl(0, TIOCGWINSZ, &ws);
printf("width: %d, height: %d\n", ws.ws_col, ws.ws_row);

return 0;
}

上面的代码中,ioctl() 函数接受三个参数:文件描述符 0(指定标准输入设备)、操作类型 TIOCGWINSZ常量、以及一个指向winsize结构体的指针。该函数执行查询操作,并将结果存储在winsize 结构体中。最后,通过打印结构体的成员变量来查看终端设备的窗口大小。

fcntl()函数用于控制文件描述符的行为。函数原型为:

int fcntl(int fd, int cmd, ...);

该函数接受三个参数:

  • fd:指定要控制的文件描述符。
  • cmd:指定要执行的操作。
  • ...:其他可选参数,用于提供更多信息,根据执行的操作而不同。

如果成功控制文件描述符,则该函数返回 0。如果控制文件描述符失败,则返回 -1。

fcntl() 函数可以执行各种不同的操作,根据所指定的操作类型而不同。例如,可以通过 fcntl() 函数设置文件描述符的状态和属性,或者控制文件锁的行为。

为了执行指定的操作,必须向 fcntl() 函数提供一个操作类型,这通常是一个由宏定义的整数值。每个操作类型对应一种特定的操作,并且需要提供一定的参数来指定操作的细节。例如,可以使用 F_GETFL 常量作为操作类型,来查询文件描述符的状态:

int status;
status = fcntl(fd, F_GETFL);

该函数可以用来实现文件锁、管道、共享内存等机制。

8.3 内存映射

mmap() 函数用于在 Linux 系统中映射文件或其他对象到进程的地址空间。该函数的原型为:

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); 

该函数接受六个参数:

  • addr:指定映射到进程地址空间的起始地址,如果设为 NULL 则由系统自动选择起始地址。

  • length:指定要映射的字节数。

  • prot:指定映射的内存页的访问权限。

  • flags:指定映射的属性和行为。

  • fd:指定要映射的文件或对象的文件描述符。

  • offset:指定要映射的文件或对象的偏移量。

如果成功映射文件或对象,则该函数返回映射的起始地址,否则返回 MAP_FAILED(一个常量,通常为-1)。

mmap() 函数用于将文件或其他对象映射到进程的地址空间,以便进程能够直接访问文件或对象的内容。这种方法可以提高文件访问的效率,因为它允许进程直接操作内存,而不需要通过内核来传输数据。

使用 mmap() 函数的一个常见用途是将一个大文件映射到内存中,以便对其进行高效的读取或修改。例如,在处理大量数据时,可以使用 mmap() 函数将数据文件映射到内存,然后直接对内存中的数据进行处理,而不需要通过磁盘读取和写入来传输数据。

使用 mmap() 函数需要注意以下几点:

  • 该函数在 Linux 系统中使用,在其他操作系统中可能不存在或有所不同。
  • 使用该函数时,必须指定要映射的文件或对象的文件描述符。
  • 在映射文件或对象之后,必须调用 munmap() 函数来取消映射,避免占用过多的内存资源。
  • 如果要修改映射的文件或对象,必须使用 msync() 函数来同步内存中的更改到文件或对象中。
  • 除了以上提到的参数和用法,mmap() 函数还有一些其他的选项和特性,具体可以参考 Linux 操作系统的相关文档。总之,mmap() 函数是一个非常有用的工具,它可以提高文件访问的效率,并为程序提供更多的灵活性和可扩展性。

例如,下面的代码演示了如何使用 mmap() 函数将一个文件映射到内存中,然后对文件进行修改并同步到文件中:

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

// 要映射的文件名
const char *FILE_NAME = "test.txt";

int main(int argc, char *argv[])
{
// 打开文件,获取文件描述符
int fd = open(FILE_NAME, O_RDWR|O_CREAT, S_IRWXU);
if (fd == -1){
perror("open() fail");
exit(EXIT_FAILURE);
}

// 获取文件信息,用于指定要映射的字节数
struct stat st;
if (fstat(fd, &st) == -1){
perror("fstat() fail");
exit(EXIT_FAILURE);
}
if (st.st_size == 0){
fprintf(stderr, "file size is 0\n");
exit(EXIT_FAILURE);
}

// 将文件映射到进程地址空间
void *p = mmap(NULL, st.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (p == MAP_FAILED){
perror("mmap() fail");
exit(EXIT_FAILURE);
}

// 对文件内容进行修改
char *s = (char *)p;
strcpy(s, "Hello, world!");

// 同步更改到文件中
msync(p, st.st_size, MS_SYNC);

// 取消映射
munmap(p, st.st_size);

// 关闭文件
close(fd);

return 0;
}