Linux系统编程01:文件I/O
本文最后更新于284 天前,其中的信息可能已经过时,如有错误请发送邮件到echobydq@gmail.com

前言

此篇文章为学习 Linux系统编程01:文件I/O 部分的笔记

1. open/close函数

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

flags

flags 用于指定文件的打开/创建模式,这个参数可由以下三个互斥的常量(定义于 fcntl.h)通过逻辑或(|)连接:

O_RDONLY      只读模式 
O_WRONLY      只写模式 
O_RDWR        读写模式

其他可选常量:

常量含义
O_APPEND每次写操作都写入文件的末尾
O_CREAT如果指定文件不存在,则创建这个文件
O_EXCL如果要创建的文件已存在,则返回 -1,并且修改 errno 的值
O_TRUNC如果文件存在,并且以只写/读写方式打开,则清空文件全部内容
O_NOCTTY如果路径名指向终端设备,不要把这个设备用作控制终端
O_NONBLOCK如果路径名指向 FIFO/块文件/字符文件,则把文件的打开和后继 I/O设置为非阻塞模式(nonblocking mode)

以下用于同步输入输出

常量含义
O_DSYNC等待物理 I/O 结束后再 write。在不影响读取新写入的数据的前提下,不等待文件属性更新
O_RSYNCread 等待所有写入同一区域的写操作完成后再进行
O_SYNC等待物理 I/O 结束后再 write,包括更新文件属性的 I/O

 

mode

mode 和 fopen() 函数的 mode 参数相同。mode 指定文件的打开模式:

r = 4,w = 2, x = 1

  • r:只读方式打开一个文本文件(该文件必须存在)
  • r+:可读可写方式打开一个文本文件(该文件必须存在)
  • w:只写方式打开一个文本文件(若文件存在则文件长度清为0,即该文件内容会消失。若文件不存在则建立该文件)
  • w+:可读可写方式创建一个文本文件(若文件存在则文件长度清为零,即该文件内容会消失。若文件不存在则建立该文件)
  • a:追加方式打开一个文本文件(若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。(EOF符保留))
  • a+:可读可写追加方式打开一个文本文件(若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾后,即文件原先的内容会被保留。 (原来的EOF符不保留))

a 和 a+ 的区别:a 不能读,a+ 可以读

  • rb:只读方式打开一个二进制文件(使用法则同r)
  • rb+:可读可写方式打开一个二进制文件(使用法则同r+)
  • wb:只写方式打开一个二进制文件(使用法则同w)
  • wb+:可读可写方式生成一个二进制文件(使用法则同w+)
  • ab:追加方式打开一个二进制文件(使用法则同a)
  • ab+:可读可写方式追加一个二进制文件(使用法则同a+)

需要注意的是,当 flags 为 O_CREAT 或 O_TMPFILE 时,必须提供 mode 参数;否则 mode 参数将不起作用。

返回值 open() 的返回值是一个 int 类型的文件描述符,打开失败返回 -1。

 

1.2 close

NAME close – 关闭一个文件描述符

总览

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

描述 close 关闭 一个文件描述符 , 使不在指向任何文件和可以在新的文件操作中被再次使用

任何与此文件相关联的以及程序所拥有的锁, 都会被删除 (忽略那些持有锁的文件描述符)

假如 fd 是最后一个文件描述符与此资源相关联 , 则这个资源将被释放.  
若此描述符是最后一个引用到此文件上的, 则文件将使用 unlink(2) 删除.

返回值

close 返回 0 表示 成功 , 或者 -1 表示 有 错误 发生 .

错误信息

EBADF  fd 不是 一个 有效 的 已 被 打开 的 文件 的 描述符

EINTR  The close() 调用 被 一 信号 中断.

EIO    I/O 有 错误 发生

 

1.3 文件权限

在Linux系统中,文件的最终权限是由文件的创建模式(mode)和用户掩码(umask)共同决定的。

文件的创建模式(mode)是指在使用系统调用如open()creat()创建文件时,通过指定一个八进制数来表示文件的权限。通常使用三个数字来表示权限,分别对应所有者、所属组和其他用户的权限。每个数字由三个位组成,分别代表读(r)、写(w)和执行(x)权限。例如,权限为 rw-r--r-- 的文件模式用八进制表示就是 644

用户掩码(umask)是用来屏蔽(取消)文件创建模式中的某些权限。默认情况下,umask 设置为 022,表示屏蔽掉写权限(w)和执行权限(x)对其他用户。所以如果文件的模式是 644,经过 umask 的处理,其他用户的权限就变为 444,即只有读权限。

计算文件的最终权限可以使用如下公式:

最终权限 = 文件模式(mode) & (~用户掩码(umask))

例如,假设文件的模式是 755,umask 是 022,则最终权限为:

755 & (~022) = 755 & 755 = 755

所以最终权限仍然是 755

另外,需要注意的是,umask 的值通常是用八进制数表示的。在设置 umask 时,一般使用四个数字,例如 umask 022

root@freecho:/opt/C/file_IO/test# umask
0022  # 755

 

1.4 代码样例

open1.c

读取文件,文件存在则以只读 O_RDONLY 方式打开,并且清空文件全部内容 O_TRUNC,如果不存在则创建文件 O_CREAT

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

int main()
{
    int fd;
    // 0644 第一个0表示8进制, 644为权限
    fd = open("./dict.cp", O_RDONLY | O_CREAT | O_TRUNC, 0644); // 644 rw-r--r--

    printf("fd = %d\n", fd);

    close(fd); // 关闭文件描述符  成功0 失败-1

    return 0;
}

/*
root@freecho:/opt/C/file_IO/test# ./open1
fd = 3
*/
root@freecho:/opt/C/file_IO/test# ./open1
fd = 3

 

open2.c

打开不存在的文件

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

int main()
{
    int fd;
    fd = open("./dict1.cp", O_RDONLY); 

    printf("fd = %d, errno = %d:%s\n", fd, errno, strerror(errno));

    close(fd); // 关闭文件描述符  成功0 失败-1

    return 0;
}
root@freecho:/opt/C/file_IO/test# ./open2
fd = -1, errno = 2:No such file or directory

 

open3.c

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

int main()
{
    int fd;

    fd = open("mydir", O_WRONLY); // 打开文件夹

    printf("fd = %d, errno = %d:%s\n", fd, errno, strerror(errno));

    close(fd); // 关闭文件描述符  成功0 失败-1

    return 0;
}
root@freecho:/opt/C/file_IO/test# ./open3
fd = -1, errno = 21:Is a directory

 

1.5 错误及处理

open 常见错误:

  1. 打开文件不存在
  2. 以写方式打开只读文件(权限问题)
  3. 以只写方式打开目录
  4. 当 open 出错时,程序会自动设置 errno,可以通过 strerror(errno)来查看报错数字的含义

错误处理函数

  1. printf("xxx error: %d\n", errno);
    
  2. char *strerror(int errnum);
    	printf("xxx error: %s\n", strerror(errno));
    
  3. void perror(const char *s);
    	perror("xxx error");
    

 

2. read/write函数

2.1 read

Man手册

NAME read – 在文件描述符上执行读操作

概述

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

描述

read( ) 从文件描述符 fd 中读取 count 字节的数据并放入从 buf 开始的缓冲区中.

如果 count 为零,read( )返回0,不执行其他任何操作. 如果 count 大于SSIZE_MAX,那么结果将不可预料.

返回值

成功时返回读取到的字节数(为零表示读到文件描述符), 此返回值受文件剩余字节数限制

当返回值小于指定的字节数时 并不意味着错误;这可能是因为当前可读取的字节数小于指定的 字节数(比如已经接近文件结尾,或者正在从管道或者终端读取数 据,或者read( )被信号中断)

发生错误时返回-1,并置 errno 为相应值.在这种情况下无法得知文件偏移位置是否有变化.

错误代码

EINTR  在读取到数据以前调用被信号所中断.

EAGAIN 使用 O_NONBLOCK 标志指定了非阻塞式输入输出,但当前没有数据可读.

EIO    输入输出错误.可能是正处于后台进程组进程试图读取其 控制终端,但读操作无效,或者被信号SIGTTIN所阻塞, 或者其进程组是孤儿进程组.也可能执行的是读磁盘或者 磁带机这样的底层输入输出错误.

EISDIR fd 指向一个目录.

EBADF  fd 不是一个合法的文件描述符,或者不是为读操作而打开.

EINVAL fd 所连接的对象不可读.

EFAULT buf 超出用户可访问的地址空间.

也可能发生其他错误,具体情况和 fd 所连接的对象有关.  POSIX 允许  read  在读取了一定量的数据后被信号所中断,并返回  -1(且  errno  被设置为EINTR),或者返回已读取的数据量.

2.2 write

Man手册

NAME write -在一个文件描述符上执行写操作

概述

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

描述 write 向文件描述符 fd 所引用的文件中写入 从 buf 开始的缓冲区中 count 字节的数据.

POSIX规定,当使用了write()之后再使用 read(),那么读取到的应该是更新后的数据. 但请注意并不是所有的文件系统都是 POSIX兼容的.

返回值 成功时返回所写入的字节数(若为零则表示没有写入数据).

错误时返回-1,并置errno为相应值.

若count为零,对于普通文件无任何影响,但对特殊文件将产生不可预料的后果.

错误代码

EBADF  fd 不是一个合法的文件描述符或者没有以写方式打开.

EINVAL fd 所指向的对象不可写.

EFAULT buf 不在用户可访问地址空间内.

EPIPE  fd  连接到一个管道,或者套接字的读方向一端已关闭.此时写进程  将接收到  SIGPIPE  信号;如果此信号被捕获,阻塞或忽略,那么将返回错误EPIPE.

EAGAIN 读操作阻塞,但使用 O_NONBLOCK 指定了非阻塞式输入输出.

EINTR  在写数据以前调用被信号中断.

ENOSPC fd 指向的文件所在的设备无可用空间.

EIO    当编辑一个节点时发生了底层输入输出错误.

可能发生了其他错误,取决于 fd 所连接的对象.

2.3 代码样例

mycp.c

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

int main(int argc, char *argv[])
{
    char buf[1024];

    int n =0;

    int fd1 = open(argv[1], O_RDONLY); // read
    if (fd1 == -1) {
        perror("open argv1 error");
        exit(1);
    }

    int fd2 = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0664); // rw-rw--r
    if (fd1 == -1) {
        perror("open argv2 error");
        exit(1);
    }

    // read 返回为读取到的字节数
    while ((n = read(fd1, buf, 1024)) != 0) { // 0:读到文件结尾结束循环
        if (n < 0) {
            perror("read error");
            break;
        }
        write(fd2, buf, n);
    }

    close(fd1);
    close(fd2);

    return 0;
}
root@freecho:/opt/C/file_IO/rw_test# make
gcc mycp.c -o mycp
root@freecho:/opt/C/file_IO/rw_test# ls
makefile  mycp  mycp.c  test1.txt
root@freecho:/opt/C/file_IO/rw_test# ./mycp test1.txt test2.txt
root@freecho:/opt/C/file_IO/rw_test# ls
makefile  mycp  mycp.c  test1.txt  test2.txt
root@freecho:/opt/C/file_IO/rw_test# cat test2.txt
Hello 这是一个cp测试文件
root@freecho:/opt/C/file_IO/rw_test# ./mycp 123
open argv1 error: No such file or directory

 

2.4 strace命令

strace是一个用于跟踪和分析程序系统调用的工具。它在Linux系统上广泛使用,可用于诊断程序执行时的问题,分析程序与操作系统之间的交互,以及定位程序的错误和性能瓶颈。strace可以帮助开发人员和系统管理员深入了解程序的运行情况,包括系统调用、信号、文件操作、网络通信等。

使用strace命令时,它会启动被跟踪的目标程序,并输出程序执行过程中的系统调用和信号等相关信息。可以用于追踪应用程序、脚本、甚至是其他系统命令的执行。

常见用法:

strace <command>

示例:

strace -o output.txt ls  # 将输出重定向到文件
strace -e trace=open,read,write ls  # 只跟踪指定的系统调用
strace -p <PID>  # 跟踪正在运行的进程

strace的输出会显示程序执行过程中每个系统调用的结果、参数和返回值,以及相应的错误信息。通过分析strace的输出,可以帮助定位程序运行时的问题,识别潜在的错误或性能瓶颈,进而进行适当的优化和调试。

 

2.5 预读入缓输出

通过比较 fgetc/fputcread/write 的执行速度,了解预读入缓输出

库函数: getc_cmp_read.c

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE *fp, *fp_out;
    int n;

    fp = fopen("test.txt", "r");
    if (fp == NULL) {
        perror("fopen error");
        exit(1);
    }

    fp_out = fopen("test.cp", "w");
    if (fp_out == NULL) {
        perror("fopen error");
        exit(1);
    }

    while ((n = fgetc(fp)) != EOF) {
        fputc(n, fp_out);
    }

    fclose(fp);
    fclose(fp_out);

    return 0;
}

系统调用:read_cmp_getc.c

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

#define N 1

int main(int argc, char *argv[])
{
    int fd, fd_out;
    int n;
    char buf[N];

    fd = open("test.txt", O_RDONLY); // read
    if (fd < 0) {
        perror("open test.txt error");
        exit(1);
    }

    fd_out = open("test.cp", O_WRONLY | O_CREAT | O_TRUNC, 0664); // rw-rw--r
    if (fd_out < 0) {
        perror("open test.cp error");
        exit(1);
    }

    // read 返回为读取到的字节数
    while ((n = read(fd, buf, N))) { // 0:读到文件结尾结束循环
        if (n < 0) {
            perror("read error");
            break;
        }
        write(fd_out, buf, n);
    }

    close(fd);
    close(fd_out);

    return 0;
}
strace -o read.txt ./read_cmp_getc
strace -o getc.txt ./getc_cmp_read
  1. readwrite系统调用是无用户缓冲区的,它们直接在用户空间和内核空间之间传递数据,每次操作只处理一个字节或一组字节,需要频繁地在用户空间和内核空间之间切换,因此在处理大量数据时可能会比较慢。
  2. fgetcfputc是C标准库提供的函数,它们使用了用户缓冲区。C标准库在打开文件时会为文件分配一个缓冲区,当使用fgetc读取一个字符或使用fputc写入一个字符时,实际上是将数据读取到用户缓冲区或从用户缓冲区写入数据。当用户缓冲区满了或遇到fflushfclose等情况时,数据才会被传输到内核。
  3. 在大多数情况下,fgetcfputc的性能相对于readwrite会慢一些,因为它们多了一层用户缓冲区的处理。但在某些情况下,特别是频繁读写小量数据时,使用C标准库提供的函数可以减少系统调用的次数,从而提高性能。

综上所述,readwrite是直接的系统调用,没有用户缓冲区,适用于大量数据的读写;而fgetcfputc是C标准库提供的函数,使用了用户缓冲区,适用于频繁读写小量数据。选择使用哪种方法取决于具体的应用场景和性能需求。

3. 文件描述符

此部分主要由 chatGPT 生成讲解

3.1 PCB进程控制块

PCB(Process Control Block,进程控制块)是操作系统中用于管理和维护进程信息的 数据结构。每个运行的进程都对应一个唯一的PCB,PCB记录了进程的状态、标识符、优先级、寄存器值、程序计数器、内存分配信息、打开的文件、CPU占用时间等信息。

PCB是操作系统实现多道程序设计和进程调度的基础。当一个进程被调度执行时,操作系统会将当前进程的上下文(包括寄存器状态等)保存在该进程的PCB中,然后加载要执行的进程的上下文,使得新进程可以继续执行。当操作系统决定暂停或切换进程时,会再次保存当前进程的上下文到其PCB中,同时加载下一个要执行的进程的上下文。

PCB 实际上是一个结构体:struct task_struct { 结构体

PCB 通常包含以下字段:

  1. 进程标识符(Process ID,PID):用于唯一标识进程的整数值。
  2. 程序计数器(Program Counter,PC):指向当前执行指令的地址。
  3. 寄存器值:保存进程的寄存器状态,包括通用寄存器、程序状态字(PSW)等。
  4. 进程状态:表示进程的当前状态,如运行、就绪、阻塞等。
  5. 进程优先级:用于进程调度时确定进程的优先级顺序。
  6. 内存分配信息:记录进程占用的内存地址空间。
  7. 打开的文件列表:记录进程打开的文件及其 文件描述符
  8. CPU占用时间:记录进程在CPU上执行的时间。
  9. 其他控制信息:可能包含信号量、消息队列、进程间通信等信息。

PCB在操作系统中起着重要的作用,它是实现多任务、多进程的基础,也是操作系统对进程进行管理和调度的核心数据结构。每个进程都有一个对应的PCB,当进程从运行态切换到阻塞态或就绪态时,PCB中的信息会被更新和保存,以便在合适的时候重新调度和恢复进程的执行。

 

3.2 文件描述符表

文件描述符

文件描述符是在操作系统中用于标识打开文件的整数值。在Unix-like系统中,包括Linux,每个打开的文件都会被分配一个文件描述符,用于标识该文件。文件描述符是一个非负整数,通常由操作系统管理。

在C语言中,使用int类型来表示文件描述符。文件描述符的值为0、1和2通常有特殊意义:

  • 文件描述符0表示标准输入(stdin),它是程序从终端接收输入数据的文件描述符。
  • 文件描述符1表示标准输出(stdout),它是程序向终端输出数据的文件描述符。
  • 文件描述符2表示标准错误(stderr),它用于向终端输出错误信息。

除了标准输入、标准输出和标准错误外,程序还可以通过系统调用(例如openreadwrite等)打开其他文件,从而得到相应的文件描述符。每个新打开的文件都会获得一个尚未使用的最小的非负整数值作为其文件描述符。

文件描述符的主要作用是用于标识文件,从而在程序中进行文件的读取、写入和关闭等操作。在C语言中,通常使用文件描述符来操作文件,例如使用readwrite来读写文件内容,使用close来关闭文件。文件描述符是一个重要的概念,它使得程序可以方便地管理多个打开的文件,并进行相应的文件操作。

文件描述符表

文件描述符表是操作系统中用于管理进程打开的文件的数据结构。在Unix-like操作系统中,每个进程都有一个文件描述符表,它是一个数组,其中的 每个元素都是一个文件描述符

数组下标可以看成指针,指向文件结构体struct file

struct file {
    
}

操作系统隐藏文件结构体内容,只暴露 数组下标,即文件描述符给用户

文件描述符表的大小是由操作系统预先定义的,并且有一定的限制。在Linux系统中,通常默认情况下,每个进程最多可以打开 1024 个文件,这个限制可以通过修改系统参数来调整。

当进程执行一个新程序时,它会继承原有的文件描述符表。这意味着在新程序中也可以继续使用原有的文件描述符来操作文件。

文件描述符表是操作系统管理进程文件操作的重要机制,它使得进程可以方便地访问和操作打开的文件,从而实现了进程与文件之间的交互和通信。

 

3.3 最大打开文件数

最大打开文件数是操作系统对一个进程能够同时打开的文件数量进行限制的值。这个限制是为了保证系统资源的合理分配和控制,防止某个进程滥用资源导致系统资源耗尽。

在Unix-like系统中,包括Linux和macOS等,最大打开文件数由系统参数ulimit来控制。ulimit命令用于设置或显示用户的资源限制,其中包括最大打开文件数。

要查看当前用户的最大打开文件数,可以在终端中运行以下命令:

ulimit -n

通常情况下,ulimit -n 的默认值为1024,即每个进程最多可以同时打开1024个文件。这个值对于普通用户来说已经足够了,但对于某些特殊应用或服务器程序来说可能会不够。

如果需要增加最大打开文件数的限制,可以使用 ulimit 命令进行设置,但是普通用户通常只能增加到一定的限制,超过系统默认值需要管理员权限。

在Linux系统中,还可以通过修改系统配置文件来增加最大打开文件数的限制。这个配置文件通常是 /etc/security/limits.conf,可以在其中添加类似下面的配置:

*       soft    nofile  65535
*       hard    nofile  65535

这里的 65535 是新的最大打开文件数限制值。设置后需要重新登录或重启系统才能生效。

需要注意的是,增加最大打开文件数的限制会占用更多的系统资源,因此在修改配置时要慎重考虑,确保系统有足够的资源支持。不当的配置可能会导致系统性能下降或资源耗尽问题。

 

3.4 FILE结构体

FILE结构体是C标准库中用于文件操作的重要数据结构。它定义在 stdio.h 头文件中,并由C库提供文件读写函数所使用。

在标准C库中,FILE结构体的定义如下:

typedef struct _IO_FILE FILE;

具体的FILE结构体的定义会因不同的编译器和操作系统而有所不同,但通常会包含用于管理文件操作的各种成员变量。

FILE结构体的主要作用是用于维护文件的状态信息,例如文件指针位置、缓冲区、读写模式等。通过FILE结构体,C库能够实现对文件的高效读写操作,并且对于程序员来说,无需直接处理底层的文件描述符,只需使用FILE指针即可。

C标准库中提供了许多基于FILE结构体的文件读写函数,例如:

  • fopen:打开文件并返回一个FILE指针。
  • fclose:关闭文件。
  • fgetc、getc:从文件中读取一个字符。
  • fgets:从文件中读取一行字符串。
  • fputc、putc:将一个字符写入文件。
  • fputs:将字符串写入文件。
  • fprintf:格式化输出到文件。
  • fread、fwrite:二进制读写数据。
  • fseek、ftell:文件指针定位。

使用这些函数,可以方便地进行文件的读写操作,而无需直接操作文件描述符。当然,底层仍然是通过文件描述符来进行实际的文件读写操作,但这一过程对于程序员来说是透明的。

需要注意的是,在使用标准C库的文件读写函数时,要注意及时关闭文件以释放资源,并检查函数返回值以处理可能出现的错误情况。

 

4. 阻塞、非阻塞

4.1 概念

读常规文件是不会阻塞的,不管读多少字节,read 一定会在有限的时间内返回。从终端设备或网络读则不一定,如果从终端输入的数据没有换行符,调用 read 读终端设备就会阻塞,如果网络上没有接收到数据包,调用 read 从网络读就会阻塞,至于会阻塞多长时间 也是不确定的,如果一直没有数据到达就一直阻塞在那里。同样,写常规文件是不会阻塞的, 而向终端设备或网络写则不一定。

现在明确一下阻塞(Block)这个概念。当进程调用一个阻塞的系统函数时,该进程被置于睡(Sleep)状态,这时内核调度其它进程运行,直到该进程等待的事件发生了(比如网络上接收到数据包,或者调用 sleep 指定的睡眠时间到了)它才有可能继续运行。与睡眠状态相对的是运行(Running)状态,在 Linux 内核中,处于运行状态的进程分为两种情况:

  1. 正在被调度执行:CPU 处于该进程的上下文环境中,程序计数器(eip)里保存着该进 程的指令地址,通用寄存器里保存着该进程运算过程的中间结果,正在执行该进程的指令, 正在读写该进程的地址空间。
  2. 就绪状态:该进程不需要等待什么事件发生,随时都可以执行,但 CPU 暂时还在执行 另一个进程,所以该进程在一个就绪队列中等待被内核调度。系统中可能同时有多个就绪的 进程,那么该调度谁执行呢?内核的调度算法是基于优先级和时间片的,而且会根据每个进 程的运行情况动态调整它的优先级和时间片,让每个进程都能比较公平地得到机会执行,同 时要兼顾用户体验,不能让和用户交互的进程响应太慢。

阻塞、非阻塞是设备文件、网络文件的属性

产生阻塞的场景:读设备文件、读网络文件。(读常规文件无阻塞概念) 补:/dev/tty --- 终端文件

注意,阻塞与非阻塞是对于文件而言的。而不是 read、write 等的属性。read 终端,默认阻塞读。

 

4.2 代码样例

block_readtty.c:阻塞阻塞读终端

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

int main()
{
    char buf[10];
    int n;

    // #define STDIN_FILENO 0  STDOUT_FILENO 1  STDERR_FILENO 2
    n = read(STDIN_FILENO, buf, 10);
    if (n < 0) {
        perror("read STDIN_FILENO");
        // printf("%d", errno);
        exit(1);
    }
    write(STDOUT_FILENO, buf, n);

    return 0;
}

nonblock_readtty.c:非阻塞读终端,通过 open 函数 改变文件状态

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

int main()
{
    char buf[10];
    int fd, n;

    fd = open("/dev/tty", O_RDONLY | O_NONBLOCK); // 设置非阻塞状态
    if (fd < 0) {
        perror("open /dev/tty");
        exit(1);
    }

tryagain:

    n = read(fd, buf, 10);

    // 如果read返回-1,并且 errno = EAGIN 或 EWOULDBLOCK,
    // 说明不是read失败,而是read在以非阻塞方式读一个设备或网络文件,并且文件无数据
    if (n < 0) {
        if (errno != EAGAIN) {        // if (errno != EWOULDBLOCK)
            perror("read /div/tty");
            exit(1);
        } else {
            write(STDOUT_FILENO, "try again\n", strlen("try again\n"));
            sleep(2);
            goto tryagain;
        }
    }

    write(STDOUT_FILENO, buf, n);
    close(fd);

    return 0;
}

nonblock_timeout.c:非阻塞读终端和等待超时

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

#define MSG_TRY "try again\n"
#define MSG_TIMEOUT "time out\n"

int main()
{
    char buf[10];
    int fd, n, i;

    fd = open("/dev/tty", O_RDONLY | O_NONBLOCK); // 设置非阻塞状态
    if (fd < 0) {
        perror("open /dev/tty");
        exit(1);
    }
    printf("open /dev/tty ok ... %d\n", fd);

    for (i = 0; i < 5; i++) {
        n = read(fd, buf, 10);
        if (n > 0) {               // 说明读到了东西
            break;
        }
        if (errno != EAGAIN) {          // if (errno != EWOULDBLOCK)
            perror("read /dev/tty");
            exit(1);
        } else {
            write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
            sleep(2);
        }
    }

    if (i == 5) {
            write(STDOUT_FILENO, MSG_TIMEOUT, strlen(MSG_TIMEOUT));
    } else {
        write(STDOUT_FILENO, buf, n);
    }

    close(fd);

    return 0;
}

 

5. fcntl 函数

5.1 概念

fcntl 函数是用于操作文件描述符的系统调用,它可以用来执行各种与文件描述符相关的操作。在C语言中,我们可以通过 fcntl 函数来调用这个系统调用。

函数原型

#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );

参数说明

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

  • cmd:指定要执行的操作,可以是下列常量之一:

    • F_DUPFD:复制文件描述符。
    • F_GETFD:获取文件描述符标志。
    • F_SETFD:设置文件描述符标志。
    • F_GETFL:获取文件状态标志。
    • F_SETFL:设置文件状态标志。
    • F_GETOWN:获取异步I/O所有权。
    • F_SETOWN:设置异步I/O所有权。
    • F_GETLK:获取文件锁信息。
    • F_SETLK:设置文件锁。
    • F_SETLKW:设置文件锁,但如果锁不可用,则阻塞。
  • arg:根据操作类型 cmd 的不同,可能需要传入其他参数。

返回值

  • 如果执行成功,则根据操作类型 cmd 的不同,返回值也不同。
  • 如果发生错误,则返回 -1,并设置 errno 来指示错误类型。

 

5.2 代码样例

下面以 F_GETFLF_SETFL 操作为例,演示如何使用 fcntl 函数来获取和设置文件状态标志:

fcntl_1.c

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

int main() {
    int fd = open("example.txt", O_RDONLY);  // 打开文件 example.txt,只读模式

    // 获取文件状态标志
    int flags = fcntl(fd, F_GETFL);
    if (flags == -1) {
        perror("fcntl");
        return 1;
    }

    printf("File flags before modification: %d\n", flags);

    // 添加 O_APPEND 标志
    flags |= O_APPEND;
    
    // 设置文件状态标志
    int result = fcntl(fd, F_SETFL, flags);
    if (result == -1) {
        perror("fcntl");
        return 1;
    }

    printf("File flags after modification: %d\n", flags);

    close(fd);
    return 0;
}

在这个例子中,我们首先打开了一个文件 example.txt,然后使用 fcntl 函数获取文件状态标志,接着添加 O_APPEND 标志,最后再次使用 fcntl 函数将修改后的标志设置回去。运行此程序,可以看到文件状态标志的变化。

下面再来一个例子

fcntl_2.c:将文件改为非阻塞状态

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

#define MSG_TRY "try again\n"

int main(void)
{
    char buf[10];
    int flags, n;

    flags = fcntl(STDIN_FILENO, F_GETFL); // 获取 stdin 属性信息

    if (flags == -1) {
        perror("fcntl error");
        exit(1);
    }

    flags |= O_NONBLOCK; // 添加上非阻塞状态
    int ret = fcntl(STDIN_FILENO, F_SETFL, flags);

    if (ret == -1) {
        perror("fcntl error");
        exit(1);
    }

tryagain:
    n = read(STDIN_FILENO, buf, 10);

    if (n < 0) {
        if (errno != EAGAIN) {
            perror("read /dev/tty");
            exit(1);
        }
        sleep(3);
        write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
        goto tryagain;
    }

    write(STDOUT_FILENO, buf, n);

    return 0;
}

 

6. lseek函数

6.1 概念

lseek 函数用于在打开的文件中定位文件指针的位置。它在 C 语言中是一个系统调用,提供了对文件读写位置的控制。

函数原型

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

参数说明

  • fd:文件描述符,是文件在进程中的标识符。

  • offset:指定了文件指针的偏移量。这个值可以为正、负或零,具体取决于 whence 参数。

  • whence:用于确定 offset 如何解释,它可以取以下值:

    • SEEK_SET:从文件开头开始偏移。
    • SEEK_CUR:从当前文件指针位置开始偏移。
    • SEEK_END:从文件末尾开始偏移。

返回值

lseek 函数返回新的文件指针位置,若出错则返回 -1,并设置全局变量 errno 来表示错误类型。

特别的:lseek 允许超过文件结尾设置偏移量,文件会因此被拓展。

注意文件“读”和“写”使用同一偏移位置。

 

6.2 代码样例

lseek1.c :文件读写使用同一偏移位置

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

int main()
{
    int fd, n;
    char msg[] = "It's a test for lseek\n";
    char ch;

    fd = open("test1.txt", O_RDWR | O_CREAT, 0644);
    if (fd < 0) {
        perror("open lseek.txt error");
        exit(1);
    }

    write(fd, msg, strlen(msg)); // 使用fd对打开的文件进行写操作,读写位置位于文件结尾处

    lseek(fd, 0, SEEK_SET);   // 修改文件读写位置,位于文件开头

    while ((n = read(fd, &ch, 1))) {
        if (n < 0) {
            perror("read error");
            exit(1);
        }
        write(STDOUT_FILENO, &ch, n); // 将文件内容按字节读出,写出到屏幕
    }

    close(fd);

    return 0;
}

lseek2.c :获取文件大小

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

int main(int argc, char *argv[])
{
    int fd = open(argv[1], O_RDWR);
    if (fd == -1) {
        perror("open error");
        exit(1);
    }

    int lenth = lseek(fd, 0, SEEK_END);
    printf("file size: %d\n", lenth);

    close(fd);

    return 0;
}

lseek3.c :拓展文件大小:要想使文件大小真正拓展,必须引起 IO 操作

也可以使用 truncate函数 直接拓展文件

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

int main(int argc, char *argv[])
{
    int fd = open(argv[1], O_RDWR);
    if (fd == -1) {
        perror("open error");
        exit(1);
    }

    int lenth = lseek(fd, 110, SEEK_END);
    printf("file size: %d\n", lenth);

    write(fd, "\0", 1);


    close(fd);

    return 0;
}

补充:

od -tcx filename # 查看文件的 16 进制表示形式 
od -tcd filename # 查看文件的 10 进制表示形式

 

7. ioctl.c 函数 (嵌入式)

7.1 概念

ioctl 是一个在 UNIX/Linux 系统中用于设备控制的函数。它用于与设备驱动程序进行通信,进行设备的配置、状态查询和控制等操作。

函数原型

#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);

参数解释

  • fd:文件描述符,用于指定要控制的设备。
  • request:一个无符号长整型参数,表示控制请求的编号。通常使用预定义的常量或者自定义的控制码。
  • ...:可选参数,如果 request 需要传递额外的数据,可以使用可变参数传递。

工作原理

ioctl 函数的作用是向设备驱动程序发送特定的控制命令(request),然后设备驱动程序根据接收到的命令执行相应的操作。这些操作可能包括设备的配置、状态查询、性能优化等。不同的设备驱动程序支持的控制命令是不同的,因此在使用 ioctl 函数时,需要参考设备驱动程序的文档或者相关头文件来了解可用的控制命令。

使用场景

ioctl 函数通常用于与特殊设备进行交互,例如硬件设备(如串口、USB 设备等)、网络设备、字符设备、以及其他类型的外设等。通过 ioctl 函数,用户可以向设备驱动程序发送自定义的控制命令,从而实现设备的灵活控制和配置。

注意事项

使用 ioctl 函数需要小心,因为它是一个较为底层的接口,如果使用不当,可能导致系统崩溃或不稳定。在使用 ioctl 函数时,建议查阅相关的文档和资料,了解设备驱动程序支持的控制命令和参数,以及正确的使用方法。

 

7.2 代码样例

ioctl.c

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

int main() {
    int fd = open("/dev/ttyS0", O_RDWR); // 打开串口设备文件

    if (fd < 0) {
        perror("open");
        return 1;
    }

    // 设置串口波特率为 9600
    int baud_rate = 9600;
    if (ioctl(fd, TIOCSBRSRATE, &baud_rate) < 0) {
        perror("ioctl");
        return 1;
    }

    // 关闭设备文件
    close(fd);
    return 0;
}

 

8. 传入传出参数

在编程中,函数的参数可以分为 传入参数(Input Parameter)传出参数(Output Parameter)传入传出参数(Input/Output Parameter) 三种类型。

这些参数的区别在于函数如何使用它们以及对它们的修改和返回。

8.1 传入参数

  1. 指针作为函数
  2. 通常有 const 关键字修饰
  3. 指针指向有效区域,在函数内部做读操作

8.2 传出参数

  1. 指针作为函数
  2. 在函数调用之前,指针指向的空间可以无意义,但必须有效
  3. 在函数内部,做写操作
  4. 函数调用结束后,充电函数返回值

8.3 传入传出参数

  1. 指针作为函数
  2. 在函数调用之前,指针指向的空间有实际意义
  3. 在函数内部,先做读操作,后做写操作
  4. 函数调用结束后,充当函数返回值

 

9. 扩展阅读

关于虚拟 4G 内存的描述和解析

此部分内容可以在学到 进程 时观看

一个进程用到的虚拟地址是由内存区域表来管理的,实际用不了 4G。而用到的内存区域,会通过页表映射到物理内存

所以每个进程都可以使用同样的虚拟内存地址而不冲突,因为它们的物理地址实际上是不同的。内核用的是 3G 以上的 1G 虚拟内存地址

其中896M 是直接映射到物理地址的,128M 按需映射 896M 以上的所谓高位内存。各进程使用的是同一个内核。

首先要分清 可以寻址实际使用 的区别。

其实我们讲的每个进程都有 4G 虚拟地址空间,讲的都是“可以寻址”4G,意思是虚拟地址的 0-3G 对于一个进程的用户态和内核态来说是可以访问的,而 3-4G 是只有进程的内核态可以访问的。并不是说这个进程会用满这些空间。

其次,所谓“独立拥有的虚拟地址”是指对于每一个进程,都可以访问自己的 0-4G 的 虚拟地址。虚拟地址是“虚拟”的,需要转化为“真实”的物理地址。

好比你有你的地址簿,我有我的地址簿。你和我的地址簿都有 1、2、3、4 页,但是每 页里面的实际内容是不一样的,我的地址簿第 1 页写着 3 你的地址簿第 1 页写着 4,对于你、 我自己来说都是用第 1 页(虚拟),实际上用的分别是第 3、4 页(物理),不冲突。

内核用的 896M 虚拟地址是直接映射的,意思是只要把虚拟地址减去一个偏移量(3G) 就等于物理地址。同样,这里指的还是寻址,实际使用前还是要分配内存。而且 896M 只是 个最大值。如果物理内存小,内核能使用(分配)的可用内存也小。

 

相关链接

教程视频:Linux系统编程哔哩哔哩bilibili

Linux系列文章:Linux – Echo (liveout.cn)

Linux基础命令:Linux学习资料分享 – Echo (liveout.cn)

GCC、GDB、Makefile:GCC、GDB、Makefile学习笔记 – Echo (liveout.cn)

Vim配置:Linux学习资料分享 – Echo (liveout.cn)

GitHub仓库,包含教程讲义、代码以及笔记:https://github.com/PGwind/LinuxSystem

觉得有帮助可以投喂下博主哦~感谢!
作者:Echo
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0协议
转载请注明文章地址及作者哦~

评论

  1. 10 月前
    2023-7-31 22:20:33

    底层逻辑好详细啊!

    来自北京
    • Avatar photo
      博主
      TeacherDu
      10 月前
      2023-7-31 22:29:52

      哈哈,感谢杜老师夸奖
      这个课程视频和讲义都挺不错的

      来自江苏

发送评论(请正确填写邮箱地址,否则将会当成垃圾评论处理) 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇