好的,我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 ioctl 函数,它是一个通用的设备输入/输出控制接口,允许程序对各种设备(如终端、文件系统、网络接口、USB 设备等)执行特定的控制操作,这些操作通常超出了基本的 readwrite 范用范围。


1. 函数介绍 链接到标题

ioctl(Input/Output Control)是一个非常通用且强大的 Linux 系统调用。它的设计目的是为设备驱动程序提供一个标准的接口,以便用户空间程序可以向内核中的设备驱动发送特定的控制命令

你可以将 ioctl 想象成一个“万能遥控器”,它可以对不同类型的“电器”(设备)执行特定的“功能”(命令),比如调整电视的音量、切换电视频道,或者设置网络接口的 IP 地址。每个设备驱动程序都会定义自己支持的一套 ioctl 命令。

由于其通用性,ioctl 的具体功能完全取决于文件描述符所关联的设备类型以及传递给它的命令 (request) 参数


2. 函数原型 链接到标题

#include <sys/ioctl.h> // 通常需要,包含各种标准 ioctl 命令定义
// 有些特定设备的 ioctl 命令可能在其他头文件中定义,如 <linux/fs.h>, <termios.h> 等

int ioctl(int fd, unsigned long request, ...);

注意: 在一些文档或旧代码中,你可能会看到 int request,但标准和现代用法推荐使用 unsigned long


3. 功能 链接到标题

  • 发送控制命令: 向与文件描述符 fd 关联的底层设备驱动程序发送一个特定的控制命令 request
  • 传递参数: 根据 request 命令的需要,可以传递额外的参数给设备驱动。这些参数通常是指向数据结构或简单值的指针。
  • 执行设备特定操作: 设备驱动程序接收到 ioctl 调用后,会解析 request 命令和附加参数,然后执行相应的硬件控制或状态查询操作。
  • 返回结果: ioctl 调用可以返回一个整数值给用户空间程序,表示操作是否成功或返回一些状态信息。

4. 参数 链接到标题

  • int fd: 这是已打开设备或资源的文件描述符。这个 fd 决定了 ioctl 调用将作用于哪个设备驱动。例如,一个终端设备的 fd、一个磁盘设备的 fd 或一个网络套接字的 fd
  • unsigned long request: 这是设备特定的请求代码(命令)。它唯一标识了要执行的操作。这些代码通常由设备驱动程序的开发者定义,并在相应的头文件中公开。
    • 编码规则: Linux 内核定义了一套 ioctl 命令号的编码规则,以确保不同设备的命令号不冲突。一个典型的命令号由以下几部分组成(使用 _IO, _IOR, _IOW, _IOWR 宏定义):
      • 类型 (Type): 一个 8 位的幻数 (magic number),用于标识设备类型或驱动程序。
      • 编号 (Number): 一个 8 位的序号,用于区分同一类型下的不同命令。
      • 方向 (Direction): 2 位,指示数据传输方向:无数据 (_IO)、读出 (_IOR)、写入 (_IOW)、读写 (_IOWR)。
      • 大小 (Size): 14 位,表示涉及的数据结构的大小(如果有的话)。
    • 常见来源: 这些 request 常量定义在各种头文件中,例如:
      • <termios.h>: 终端控制命令 (如 TCGETS, TCSETS)。
      • <linux/fs.h>: 文件系统相关命令 (如 BLKGETSIZE64)。
      • <linux/if.h>: 网络接口命令。
      • 设备特定的头文件。
  • ... (可变参数): 这代表可选的第四个参数。根据 request 命令的要求,这个参数可能是:
    • 一个指向数据结构的指针(用于传递复杂参数或接收返回数据)。
    • 一个简单的整数值。
    • 无参数(对于某些命令)。 如果命令需要参数,文档会明确说明参数的类型和含义。

5. 返回值 链接到标题

  • 成功时: 返回值取决于具体的 request 命令。很多命令成功时返回 0,但也有些命令会返回非零的整数值作为结果(例如,查询操作可能返回查询到的值)。
  • 失败时: 通常返回 -1,并设置全局变量 errno 来指示具体的错误原因(例如 EBADF fd 无效,ENOTTY fd 不支持该 ioctl 命令,EINVAL request 或参数无效等)。

重要: 由于返回值含义多样,必须查阅具体 ioctl 命令的文档来了解其成功和失败时的返回值约定。


6. 相似函数,或关联函数 链接到标题

  • read, write: 这是最基本的文件 I/O 操作。ioctl 用于更高级的、特定于设备的控制。
  • fcntl: 用于对文件描述符本身进行操作(如设置文件状态标志、获取/设置文件描述符标志、复制文件描述符等),而不是对文件描述符所指向的设备或文件内容进行操作。fcntl 的作用域更偏向于文件描述符层面。
  • 特定设备的库函数: 许多高级库为特定类型的设备提供了封装好的函数,这些函数内部可能调用了 ioctl。例如,网络编程中的 setsockopt/getsockopt,终端编程中的 tcsetattr/tcgetattr

7. 示例代码 链接到标题

示例 1:使用 ioctl 获取终端窗口大小 链接到标题

这个例子演示如何使用 ioctl 查询当前终端(控制台或 xterm 等)的行数和列数。

#include <sys/ioctl.h>  // TIOCGWINSZ
#include <unistd.h>     // STDOUT_FILENO
#include <stdio.h>      // perror, printf
#include <stdlib.h>     // exit
#include <termios.h>    // winsize (在某些系统上可能需要)

// winsize 结构体通常定义在 <sys/ioctl.h> 或 <termios.h> 中
// struct winsize {
//     unsigned short ws_row;    // rows, in characters
//     unsigned short ws_col;    // columns, in characters
//     unsigned short ws_xpixel; // horizontal size, pixels (unused)
//     unsigned short ws_ypixel; // vertical size, pixels (unused)
// };

int main() {
    struct winsize w;

    // 调用 ioctl 获取终端窗口大小
    // fd: STDOUT_FILENO (标准输出通常连接到终端)
    // request: TIOCGWINSZ (Get Window Size 的 ioctl 命令)
    // arg: (void *)&w (指向 winsize 结构体的指针,用于接收数据)
    if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == -1) {
        perror("ioctl TIOCGWINSZ failed");
        exit(EXIT_FAILURE);
    }

    printf("Terminal size:\n");
    printf("  Rows: %d\n", (int)w.ws_row);
    printf("  Columns: %d\n", (int)w.ws_col);
    // printf("  Width (pixels): %d\n", (int)w.ws_xpixel); // 通常未使用
    // printf("  Height (pixels): %d\n", (int)w.ws_ypixel); // 通常未使用

    return 0;
}

代码解释:

  1. 包含必要的头文件 <sys/ioctl.h>
  2. 声明一个 struct winsize 结构体 w 用于接收窗口大小信息。
  3. 调用 ioctl(STDOUT_FILENO, TIOCGWINSZ, &w)
    • STDOUT_FILENO: 标准输出文件描述符,通常连接到运行程序的终端。
    • TIOCGWINSZ: 这是在 <sys/ioctl.h> 中定义的一个标准 ioctl 命令,专门用于获取终端窗口大小。
    • &w: 将结构体 w 的地址作为参数传递,ioctl 会把获取到的信息填充到这个结构体中。
  4. 检查 ioctl 的返回值。如果返回 -1,表示失败,打印错误信息并退出。
  5. 如果成功,从 w.ws_roww.ws_col 中读取行数和列数并打印。

示例 2:使用 ioctl 操作终端属性 (termios) 链接到标题

这个例子演示如何使用 ioctl 来获取和设置终端的输入模式,例如关闭行缓冲(实现“即时”字符输入)和回显。

#include <sys/ioctl.h> // 不是必须,但习惯包含
#include <termios.h>   // tcgetattr, tcsetattr, termios, TCSAFLUSH
#include <unistd.h>    // read, STDIN_FILENO
#include <stdio.h>     // perror, printf
#include <stdlib.h>    // exit

int main() {
    struct termios orig_termios, raw_termios;
    char ch;

    // --- 1. 获取当前终端属性 ---
    if (tcgetattr(STDIN_FILENO, &orig_termios) == -1) {
        perror("tcgetattr failed");
        exit(EXIT_FAILURE);
    }
    printf("Original terminal settings retrieved.\n");

    // --- 2. 复制一份并修改为原始模式 (raw mode) ---
    raw_termios = orig_termios; // 结构体赋值

    // 关闭规范模式 (ICANON) - 这样 read() 不会等待换行符
    raw_termios.c_lflag &= ~(ICANON);
    // 关闭回显 (ECHO) - 这样输入的字符不会显示在屏幕上
    raw_termios.c_lflag &= ~(ECHO);
    // 可以根据需要关闭其他标志,这里只改这两个

    // --- 3. 应用新的终端属性 ---
    // TCSAFLUSH: 改变设置前刷新输入队列,并丢弃已写入但未发送的输出
    if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw_termios) == -1) {
        perror("tcsetattr to raw mode failed");
        exit(EXIT_FAILURE);
    }
    printf("Terminal set to raw mode (non-canonical, no echo).\n");
    printf("Press any key (q to quit): ");

    // --- 4. 读取字符 (无需按回车) ---
    while (1) {
        // read 会立即返回,因为关闭了 ICANON
        ssize_t nread = read(STDIN_FILENO, &ch, 1);
        if (nread <= 0) {
            perror("read failed");
            break;
        }
        printf("\nYou pressed: '%c' (ASCII: %d)\n", ch, (int)ch);
        if (ch == 'q' || ch == 'Q') {
            break;
        }
        printf("Press any key (q to quit): ");
    }

    // --- 5. 恢复原始终端属性 ---
    if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios) == -1) {
        perror("tcsetattr to restore original mode failed");
        // 即使失败也退出,因为终端设置可能已乱
    } else {
        printf("\nTerminal settings restored.\n");
    }

    return 0;
}

代码解释:

  1. tcgetattrtcsetattr: 虽然它们是库函数,但它们内部是通过 ioctl (TCGETS, TCSETS 等命令) 实现的。这个例子使用它们是因为它们更方便、更标准。
  2. 获取当前终端设置 (orig_termios) 并保存,以便程序结束时恢复。
  3. 复制一份设置到 raw_termios,并修改 c_lflag 字段:
    • ICANON: 规范输入模式。关闭它后,read 不会等待换行符,而是立即返回可用字符。
    • ECHO: 回显。关闭它后,按键不会显示在屏幕上。
  4. 使用 tcsetattr 应用修改后的设置。TCSAFLUSH 选项确保设置立即生效并清理缓冲区。
  5. 进入循环,使用 read(STDIN_FILENO, &ch, 1) 读取单个字符。由于设置了原始模式,read 会立即返回。
  6. 按 ‘q’ 或 ‘Q’ 退出循环。
  7. 重要: 程序结束前,使用 tcsetattr 恢复原始的终端设置。如果不恢复,终端可能会处于不正常状态(例如,输入不回显)。

示例 3:使用 ioctl 获取块设备大小 链接到标题

这个例子演示如何使用 ioctl 查询一个块设备(如硬盘分区)的大小。

#include <sys/ioctl.h> // BLKGETSIZE64
#include <fcntl.h>     // open, O_RDONLY
#include <unistd.h>    // close
#include <stdio.h>     // perror, printf
#include <stdlib.h>    // exit
#include <linux/fs.h>  // 包含 BLKGETSIZE64 的定义

int main(int argc, char *argv[]) {
    int fd;
    uint64_t size_bytes;
    double size_gb;

    if (argc != 2) {
        fprintf(stderr, "Usage: %s <block_device_path>\n", argv[0]);
        fprintf(stderr, "Example: %s /dev/sda1\n", argv[0]);
        // 注意:直接访问 /dev/sdX 通常需要 root 权限
        exit(EXIT_FAILURE);
    }

    // 1. 以只读方式打开块设备
    // 注意:访问设备文件通常需要特殊权限 (如 root)
    fd = open(argv[1], O_RDONLY);
    if (fd == -1) {
        perror("open block device failed");
        exit(EXIT_FAILURE);
    }

    printf("Opened block device '%s' successfully.\n", argv[1]);

    // 2. 调用 ioctl 获取设备大小 (以字节为单位)
    // request: BLKGETSIZE64 (在 <linux/fs.h> 中定义)
    // arg: (void *)&size_bytes (指向 uint64_t 的指针,用于接收大小)
    if (ioctl(fd, BLKGETSIZE64, &size_bytes) == -1) {
        perror("ioctl BLKGETSIZE64 failed");
        close(fd);
        exit(EXIT_FAILURE);
    }

    // 3. 计算并打印大小
    size_gb = (double)size_bytes / (1024 * 1024 * 1024);
    printf("Size of '%s':\n", argv[1]);
    printf("  Bytes: %llu\n", (unsigned long long)size_bytes);
    printf("  Gigabytes: %.2f GB\n", size_gb);

    // 4. 关闭文件描述符
    if (close(fd) == -1) {
        perror("close block device failed");
        exit(EXIT_FAILURE);
    }

    return 0;
}

代码解释:

  1. 检查命令行参数。
  2. 使用 open() 打开指定的块设备文件(如 /dev/sda1)。这通常需要 root 权限。
  3. 调用 ioctl(fd, BLKGETSIZE64, &size_bytes)
    • fd: 块设备的文件描述符。
    • BLKGETSIZE64: 这是在 <linux/fs.h> 中定义的一个 ioctl 命令,用于获取块设备的大小(以字节为单位)。
    • &size_bytes: 指向一个 uint64_t 变量的指针,用于接收设备大小。
  4. 检查 ioctl 返回值,失败则处理错误。
  5. 将字节数转换为 GB 并打印。
  6. 关闭文件描述符。

编译和运行:

gcc -o get_block_size get_block_size.c
# 列出你的块设备
lsblk
# 运行 (可能需要 sudo)
sudo ./get_block_size /dev/sda1

总结:

ioctl 是一个极其灵活和强大的系统调用,它为用户空间程序与内核设备驱动程序之间的通信提供了一个标准接口。理解 ioctl 的关键是知道文件描述符关联的设备类型以及特定的 request 命令码。由于其通用性,使用 ioctl 时必须仔细查阅相关设备或驱动的文档。