1. 程式人生 > >linux 核心 - ioctl 函式詳解

linux 核心 - ioctl 函式詳解

1. 概念

ioctl 是裝置驅動程式中裝置控制介面函式,一個字元裝置驅動通常會實現裝置開啟、關閉、讀、寫等功能,在一些需要細分的情境下,如果需要擴充套件新的功能,通常以增設 ioctl() 命令的方式實現。

在檔案 I/O 中,ioctl 扮演著重要角色,本文將以驅動開發為側重點,從使用者空間到核心空間縱向分析 ioctl 函式。

image

2. 使用者空間 ioctl

#include <sys/ioctl.h> 

int ioctl(int fd, int cmd, ...) ;
引數 描述
fd 檔案描述符
cmd 互動協議,裝置驅動將根據 cmd 執行對應操作
可變引數 arg,依賴 cmd 指定長度以及型別

ioctl() 函式執行成功時返回 0,失敗則返回 -1 並設定全域性變數 errorno 值,如下:

EBADF d is not a valid descriptor. 
EFAULT argp references an inaccessible memory area. 
EINVAL Request or argp is
not valid. ENOTTY d is not associated with a character special device. ENOTTY The specified request does not apply to the kind of object that the descriptor d references.

因此,在使用者空間使用 ioctl 時,可以做如下的出錯判斷以及處理:

int ret;
ret = ioctl(fd, MYCMD);
if (ret == -1) {
    printf("ioctl: %s\n", strerror(errno));
}

在實際應用中,ioctl 最常見的 errorno 值為 ENOTTY(error not a typewriter),顧名思義,即第一個引數 fd 指向的不是一個字元裝置,不支援 ioctl 操作,這時候應該檢查前面的 open 函式是否出錯或者裝置路徑是否正確

3. 驅動程式 ioctl

long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);

在新版核心中, 與 取代了 。unlocked_ioctl,顧名思義,應該在無大核心鎖(BKL)的情況下呼叫;compat_ioctl,compat 全稱 compatible(相容的),主要目的是為 64 位系統提供 32 位 ioctl 的相容方法,也是在無大核心鎖的情況下呼叫。

在《Linux Kernel Development》中對兩種 ioctl 方法有詳細的解說。

在字元裝置驅動開發中,一般情況下只要實現 unlocked_ioctl 函式即可,因為在 vfs 層的程式碼是直接呼叫 unlocked_ioctl 函式

// fs/ioctl.c

static long vfs_ioctl(struct file *filp, unsigned int cmd,
              unsigned long arg)
{
    int error = -ENOTTY;

    if (!filp->f_op || !filp->f_op->unlocked_ioctl)           
        goto out;

    error = filp->f_op->unlocked_ioctl(filp, cmd, arg);
    if (error == -ENOIOCTLCMD) {
        error = -ENOTTY;
    }   
 out:
    return error;
}

4. ioctl 使用者與驅動之間的協議

前文提到 ioctl 方法第二個引數 cmd 為使用者與驅動的 “協議”,理論上可以為任意 int 型資料,可以為 0、1、2、3……,但是為了確保該 “協議” 的唯一性,ioctl 命令應該使用更科學嚴謹的方法賦值,在linux中,提供了一種 ioctl 命令的統一格式,將 32 位 int 型資料劃分為四個位段,如下圖所示:

image

在核心中,提供了巨集介面以生成上述格式的 ioctl 命令:

// include/uapi/asm-generic/ioctl.h

#define _IOC(dir,type,nr,size) \
    (((dir)  << _IOC_DIRSHIFT) | \
     ((type) << _IOC_TYPESHIFT) | \
     ((nr)   << _IOC_NRSHIFT) | \
     ((size) << _IOC_SIZESHIFT))
  1. dir(direction),ioctl 命令訪問模式(資料傳輸方向),佔據 2 bit,可以為 _IOC_NONE、_IOC_READ、_IOC_WRITE、_IOC_READ | _IOC_WRITE,分別指示了四種訪問模式:無資料、讀資料、寫資料、讀寫資料;
  2. type(device type),裝置型別,佔據 8 bit,在一些文獻中翻譯為 “幻數” 或者 “魔數”,可以為任意 char 型字元,例如
    ‘a’、’b’、’c’ 等等,其主要作用是使 ioctl 命令有唯一的裝置標識;
  3. nr(number),命令編號/序數,佔據 8 bit,可以為任意 unsigned char 型資料,取值範圍 0~255,如果定義了多個 ioctl 命令,通常從 0 開始編號遞增;
  4. size,涉及到 ioctl 函式 第三個引數 arg ,佔據 13bit 或者 14bit(體系相關,arm 架構一般為 14 位),指定了 arg 的資料型別及長度,如果在驅動的 ioctl 實現中不檢查,通常可以忽略該引數;

通常而言,為了方便會使用巨集 _IOC() 衍生的介面來直接定義 ioctl 命令:

// include/uapi/asm-generic/ioctl.h

/* used to create numbers */
#define _IO(type,nr)        _IOC(_IOC_NONE,(type),(nr),0)
#define _IOR(type,nr,size)  _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOW(type,nr,size)  _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
_IO:       定義不帶引數的 ioctl 命令
_IOW:      定義帶寫引數的 ioctl 命令(copy_from_user)
_IOR:      定義帶讀引數的ioctl命令(copy_to_user)
_IOWR:     定義帶讀寫引數的 ioctl 命令

同時,核心還提供了反向解析 ioctl 命令的巨集介面:

// include/uapi/asm-generic/ioctl.h

/* used to decode ioctl numbers */
#define _IOC_DIR(nr)        (((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK)
#define _IOC_TYPE(nr)       (((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK)
#define _IOC_NR(nr)     (((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK)
#define _IOC_SIZE(nr)       (((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK)

5. ioctl_test 例項分析

本例假設一個帶暫存器的裝置,設計了一個 ioctl 介面實現裝置初始化、讀寫暫存器等功能。在本例中,為了攜帶更多的資料,ioctl 的第三個可變引數為指標型別,指向自定義的結構體 struct msg。

1、ioctl-test.h,使用者空間和核心空間共用的標頭檔案,包含 ioctl 命令及相關巨集定義,可以理解為一份 “協議” 檔案,程式碼如下:

// ioctl-test.h

#ifndef __IOCTL_TEST_H__
#define __IOCTL_TEST_H__

#include <linux/ioctl.h>    // 核心空間
// #include <sys/ioctl.h>   // 使用者空間

/* 定義裝置型別 */
#define IOC_MAGIC  'c'

/* 初始化裝置 */
#define IOCINIT    _IO(IOC_MAGIC, 0)

/* 讀暫存器 */
#define IOCGREG    _IOW(IOC_MAGIC, 1, int)

/* 寫暫存器 */
#define IOCWREG    _IOR(IOC_MAGIC, 2, int)

#define IOC_MAXNR  3

struct msg {
    int addr;
    unsigned int data;
};

#endif

2、ioctl-test-driver.c,字元裝置驅動,實現了unlocked_ioctl 介面,根據上層使用者的 cmd 執行對應的操作(初始化裝置、讀暫存器、寫暫存器)。在接收上層 cmd 之前應該對其進行充分的檢查,流程及具體程式碼實現如下:

// ioctl-test-driver.c
......

static const struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = test_open,
    .release = test_close,
    .read = test_read,
    .write = etst_write,
    .unlocked_ioctl = test_ioctl,
};

......

static long test_ioctl(struct file *file, unsigned int cmd, \
                        unsigned long arg)
{
    //printk("[%s]\n", __func__);

    int ret;
    struct msg my_msg;

    /* 檢查裝置型別 */
    if (_IOC_TYPE(cmd) != IOC_MAGIC) {
        pr_err("[%s] command type [%c] error!\n", \
            __func__, _IOC_TYPE(cmd));
        return -ENOTTY; 
    }

    /* 檢查序數 */
    if (_IOC_NR(cmd) > IOC_MAXNR) { 
        pr_err("[%s] command numer [%d] exceeded!\n", 
            __func__, _IOC_NR(cmd));
        return -ENOTTY;
    }    

    /* 檢查訪問模式 */
    if (_IOC_DIR(cmd) & _IOC_READ)
        ret= !access_ok(VERIFY_WRITE, (void __user *)arg, \
                _IOC_SIZE(cmd));
    else if (_IOC_DIR(cmd) & _IOC_WRITE)
        ret= !access_ok(VERIFY_READ, (void __user *)arg, \
                _IOC_SIZE(cmd));
    if (ret)
        return -EFAULT;

    switch(cmd) {
    /* 初始化裝置 */
    case IOCINIT:
        init();
        break;

    /* 讀暫存器 */
    case IOCGREG:
        ret = copy_from_user(&msg, \
            (struct msg __user *)arg, sizeof(my_msg));
        if (ret) 
            return -EFAULT;
        msg->data = read_reg(msg->addr);
        ret = copy_to_user((struct msg __user *)arg, \
                &msg, sizeof(my_msg));
        if (ret) 
            return -EFAULT;
        break;

    /* 寫暫存器 */
    case IOCWREG:
        ret = copy_from_user(&msg, \
            (struct msg __user *)arg, sizeof(my_msg));
        if (ret) 
            return -EFAULT;
        write_reg(msg->addr, msg->data);
        break;

    default:
        return -ENOTTY;
    }

    return 0;
}

3、ioctl-test.c,執行在使用者空間的測試程式:

// ioctl-test.c

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

#include "ioctl-test.h"

int main(int argc, char **argv)
{

    int fd;
    int ret;
    struct msg my_msg;

    fd = open("/dev/ioctl-test", O_RDWR);
    if (fd < 0) {
        perror("open");
        exit(-2);
    }

    /* 初始化裝置 */
    ret = ioctl(fd, IOCINIT);
    if (ret) {
        perror("ioctl init:");
        exit(-3);
    }

    /* 往暫存器0x01寫入資料0xef */
    memset(&my_msg, 0, sizeof(my_msg));
    my_msg.addr = 0x01;
    my_msg.data = 0xef;
    ret = ioctl(fd, IOCWREG, &my_msg);
    if (ret) {
        perror("ioctl read:");
        exit(-4);
    }

    /* 讀暫存器0x01 */
    memset(&my_msg, 0, sizeof(my_msg));
    my_msg.addr = 0x01;
    ret = ioctl(fd, IOCGREG, &my_msg);
    if (ret) {
        perror("ioctl write");
        exit(-5);
    }
    printf("read: %#x\n", my_msg.data);

    return 0;
}