和設備文件對話(寫和 IOCTL)
設備文件應該表現物理設備。大多物理設備既作為輸出也作為輸入,因此必須有某個機制使內核中的設備驅動程序得到來自進程的輸出以便發送到設備。通過為輸出打開設備文件並向其寫而做到這個,就像寫一個普通文件。在下面的例子中,這是用 device_write 實現的。
這不總是足夠的。想象你有一個串行口連接到一個調制解調器(即使你有一個內置的調制解調器,從CPU的觀點看它仍然是通過串行口連接到調制解調器,因此你不必責備你的想象力)。自然而然的事情是使用設備文件向調制解調器寫(要麼是調制解調器命令,要麼是要通過電話線發送的數據)和從中讀(要麼是命令回應,要麼是接收的數據)。然而,這留下了當你需要和串行口對話時該做什麼的問題,例如以什麼速率接收和發送數據。
在 Unix 中,答案是使用特殊的函數調用 ioctl ( input output control 的縮寫)。每個設備可以有自己的 ioctl 命令,它可以讀 ioctl (從進程向內核發送信息)和寫 ioctl(返回信息給進程)(注意在這兒讀寫的作用又是顛倒的,因此ioctl 的讀是發送消息給內核而寫是從內核接收消息)或者什麼也不做。 ioctl 使用三個參數調用: 合適的設備文件的文件描述符, ioctl 號及一個參數,該參數是類型長度,因此你可以使用一個模型傳遞任何東西。 (這是不准確的。例如你不能通過ioctl傳遞一個結構 -- 但你可以傳遞那個結構的指針)
ioctl 號用主設備號, ioctl 類型,命令和參數類型編碼。這個 ioctl 號通常用一個頭文件中的宏調用 (_IO, _IOR, _IOW 或 _IOWR -- 取決於類型)創建。頭文件必須被使用ioctl的程序(因此它們可以生成合適的ioctl)及內核模塊(因此它可以理解它) #include。 在下面的范例中,頭文件是 chardev.h 而使用它的程序是 ioctl.c。
如果你想在你自己的模塊中使用 ioctl ,最好接受官方的 ioctl 分配,因此如果你碰巧得到別人的ioctl或它們得到你的,你就可以知道某些事是錯的。需要更多信息,請參考 `Documentation/ioctl-number.txt' 內核源代碼樹。
范例 chardev.c
/* chardev.c * * 創建輸入輸出的字符設備 */ /* Copyright (C) 1998-99 by Ori Pomerantz */ /* 必要頭文件 */ /* 標准頭文件 */ #include /* 內核工作 */ #include /* 明確指定是模塊 */ /* 處理 CONFIG_MODVERSIONS */ #if CONFIG_MODVERSIONS==1 #define MODVERSIONS #include #endif /* 為了字符設備 */ /* 字符設備的定義在此 */ #include /* 目前對後面妹妹有用的包裝,但可能對未來LINUX版本的兼容性有幫助 */ #include /* 我們自己的ioctl 號 */ #include "chardev.h" /* 在 2.2.3 版/usr/include/linux/version.h 包含該宏, * 但 2.0.35版不包含-加入以備需要 */ #ifndef KERNEL_VERSION #define KERNEL_VERSION(a,b,c) ((a)*65536+(b)*256+(c)) #endif #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) #include /* 為了 get_user 和 put_user */ #endif #define SUCCESS 0 /* 設備聲明 ******************************** */ /* 將出現在 /proc/devices 中設備名 */ #define DEVICE_NAME "char_dev" /* 設備的消息的最大長度 */ #define BUF_LEN 80 /* 設備正打開?防止對同一設備的同時訪問 */ static int Device_Open = 0; /* 當被詢問時設備將給出的消息 */ static char Message[BUF_LEN]; /* 進程讀取消息到哪兒?如果消息的長度大於我們將用於填充在 device_read的緩沖區的大小,這將有用。*/ static char *Message_Ptr; /* 這個函數在進程試圖打開設備文件時被調用 */ static int device_open(struct inode *inode, struct file *file) { #ifdef DEBUG printk ("device_open(%p)\n", file); #endif /* 我們不想同時和兩個進程對話 */ if (Device_Open) return -EBUSY; /* 如果這是個進程,我們將更小心,因為一個進程可能已經剛好在另一個進程試圖增加Device_Open * 之前檢查過它。然而我們是在內核中,因此我們在上下文切換上被保護。 * * 這不是我們應該采取的態度,因為我們可能運行在一個 SMP 單元上,但我們將在後面一章處理SMP */ Device_Open++; /* 初始化消息 */ Message_Ptr = Message; MOD_INC_USE_COUNT; return SUCCESS; } /* 當一個進程關閉設備文件時該函數被調用。它沒有返回值因為它不能失敗。不要考慮其他任何事的發生 * 你總應該可以關閉一個設備(在 2.0 版中情況如此,在 2.2 版中設備文件可能不能關閉)。 */ #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) static int device_release(struct inode *inode, struct file *file) #else static void device_release(struct inode *inode, struct file *file) #endif { #ifdef DEBUG printk ("device_release(%p,%p)\n", inode, file); #endif /* 為下個調用者做准備 */ Device_Open --; MOD_DEC_USE_COUNT; #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) return 0; #endif } /* 當一個已經打開設備文件的進程試圖從它讀時該函數被調用。 */ #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) static ssize_t device_read( struct file *file, char *buffer, /* 填充數據的緩沖區 */ size_t length, /* 緩沖區長度 */ loff_t *offset) /* 文件偏移量 */ #else static int device_read( struct inode *inode, struct file *file, char *buffer, /* 填充數據的緩沖區 */ int length) /* 緩沖區長度(一定不能在寫時超過它!) */ #endif { /* 實際寫入緩沖區的字節數 */ int bytes_read = 0; #ifdef DEBUG printk("device_read(%p,%p,%d)\n", file, buffer, length); #endif /* 如果在消息尾則返回0表示文件尾 */ if (*Message_Ptr == 0) return 0; /* 實際上將數據放入緩沖區 */ while (length && *Message_Ptr) { /* 因為緩沖區在用戶數據段而不是內核的數據段,分配無法工作。替代的, * 我們使用將內核數據段中的數據拷貝到用戶數據段的 put_user 。*/ put_user(*(Message_Ptr++), buffer++); length --; bytes_read ++; } #ifdef DEBUG printk ("Read %d bytes, %d left\n", bytes_read, length); #endif /* 讀函數應該返回實際插入緩沖區的字節數 */ return bytes_read; } /* 當有人向我們的設備文件寫時該函數被調用。 */ #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) static ssize_t device_write(struct file *file, const char *buffer, size_t length, loff_t *offset) #else static int device_write(struct inode *inode, struct file *file, const char *buffer, int length) #endif { int i; #ifdef DEBUG printk ("device_write(%p,%s,%d)", file, buffer, length); #endif for(i=0; i #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) get_user(Message[i], buffer+i); #else Message[i] = get_user(buffer+i); #endif Message_Ptr = Message; /* 又一次返回使用過的輸入的字節數 */ return i; } /* 當一個進程試圖在我們的設備文件上做 ioctl 時該函數被調用。我們需要兩個額外的參數 * (附加於節點結構和文件結構,那是所有的設備函數都需要的): ioctl 號和給出 ioctl 函數的參數 * * 如果 ioctl 是寫或讀/寫(意味著輸出被返回給調用進程), ioctl 調用返回這個函數的輸出。 */ int device_ioctl( struct inode *inode, struct file *file, unsigned int ioctl_num,/* ioctl 號 */ unsigned long ioctl_param) /* 對它的參數 */ { int i; char *temp; #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) char ch; #endif /* 根據 ioctl 調用選擇 */ switch (ioctl_num) { case IOCTL_SET_MSG: /* 接收指向消息的指針(在用戶空間)並將它設為設備的消息 */ /* 得到由進程給出的給 ioctl 的參數 */ temp = (char *) ioctl_param; /* 找到消息的長度 */ #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) get_user(ch, temp); for (i=0; ch && i get_user(ch, temp); #else for (i=0; get_user(temp) && i ; #endif /* 不要重新發明車輪-調用 device_write */ #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) device_write(file, (char *) ioctl_param, i, 0); #else device_write(inode, file, (char *) ioctl_param, i); #endif break; case IOCTL_GET_MSG: /* 將當前的消息給調用進程 - 參數是一個指針,填充它 */ #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) i = device_read(file, (char *) ioctl_param, 99, 0); #else i = device_read(inode, file, (char *) ioctl_param, 99); #endif /* 警告 - 我們假設緩沖區長度是100。如果它小於那將使緩沖區溢出而導致進程傾倒核心 *(生成core文件)。 * * 我們只允許99個字符的原因是字符串終止符 NULL 也需要空間。 */ /* 將0放置在緩沖區尾使它適當的終止。 */ put_user('\0', (char *) ioctl_param+i); break; case IOCTL_GET_NTH_BYTE: /* 這個 ioctl 既輸入 (ioctl_param) 也輸出(這個函數的返回值) */ return Message[ioctl_param]; break; } return SUCCESS; } /* 模塊聲明 *************************** */ /* 這個結構將保存當進程對我們創建的設備做什麼時將調用的函數。因為這個結構的指針被保存在 * 設備表中,所以它不能對 init_module是局部的。 NULL 是為未實現的函數保留的。 */ struct file_operations Fops = { NULL, /* 偏移 */ device_read, device_write, NULL, /* 讀目錄 */ NULL, /* 選擇 */ device_ioctl, /* ioctl */ NULL, /* mmap */ device_open, #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) NULL, /* 刷新 */ #endif device_release /* 又名關閉 */ }; /* 初始化模塊 - 登記字符設備 */ int init_module() { int ret_val; /* 登記字符設備(至少是試圖登記) */ ret_val = module_register_chrdev(MAJOR_NUM, DEVICE_NAME, &Fops); /* 負數表示錯誤 */ if (ret_val < 0) { printk ("%s failed with %d\n", "Sorry, registering the character device ", ret_val); return ret_val; } printk ("%s The major device number is %d.\n", "Registeration is a success", MAJOR_NUM); printk ("If you want to talk to the device driver,\n"); printk ("you'll have to create a device file. \n"); printk ("We suggest you use:\n"); printk ("mknod %s c %d 0\n", DEVICE_FILE_NAME, MAJOR_NUM); printk ("The device file name is important, because\n"); printk ("the ioctl program assumes that's the\n"); printk ("file you'll use.\n"); return 0; } /* 清除 - 從 /proc 中注銷相關文件 */ void cleanup_module() { int ret; /* 注銷設備 */ ret = module_unregister_chrdev(MAJOR_NUM, DEVICE_NAME); /* 如果有錯誤,報告它 */ if (ret < 0) printk("Error in module_unregister_chrdev: %d\n", ret); } 范例 chardev.h /* chardev.h - 包含 ioctl 定義的頭文件。 * * 這裡的聲明必須在頭文件中,因為他們需要被內核模塊(chardev.c)和調用它的進程 (ioctl.c)知道。 */ #ifndef CHARDEV_H #define CHARDEV_H #include /* 主設備號。我們不能再依賴於動態的登記,因為 ioctl 需要知道它。 */ #define MAJOR_NUM 100 /* 設置設備驅動程序的消息 */ #define IOCTL_SET_MSG _IOR(MAJOR_NUM, 0, char *) /* _IOR 意思是我們正在為從用戶進程到內核模塊的信息創建一個 ioctl 命令號。 * * 第一個參數, MAJOR_NUM,是我們使用的主設備號。 * * 第二個參數是命令號(可能是幾個帶有不同的意思的)。 * * 第三個參數是我們想得到的從進程傳到內核的類型。 */ /* 得到設備驅動程序的消息 */ #define IOCTL_GET_MSG _IOR(MAJOR_NUM, 1, char *) /* 這個 IOCTL 用於輸出,得到設備驅動程序的消息。然而我們仍然需要緩沖區作為輸入放置消息,因為 * 它已經被進程分配了。 */ /* 得到消息的第n個字節 */ #define IOCTL_GET_NTH_BYTE _IOWR(MAJOR_NUM, 2, int) /* 這個 IOCTL 既用於輸入也用於輸出。它接受來自用戶的一個數 n ,然後返回 Message[n]。 */ /* 設備文件名 */ #define DEVICE_FILE_NAME "char_dev" #endif 范例 ioctl.c /* ioctl.c - 進程使用 ioctl 去控制內核模塊 * * 直到現在我們都使用 cat 做輸入和輸出。但是現在我們需要做 ioctl ,這需要寫自己的進程。 */ /* Copyright (C) 1998 by Ori Pomerantz */ /* 設備細節,例如 ioctl號和主設備號 */ #include "chardev.h" #include /* 打開 */ #include /* 退出 */ #include /* ioctl */ /* ioctl 調用函數 */ ioctl_set_msg(int file_desc, char *message) { int ret_val; ret_val = ioctl(file_desc, IOCTL_SET_MSG, message); if (ret_val < 0) { printf ("ioctl_set_msg failed:%d\n", ret_val); exit(-1); } } ioctl_get_msg(int file_desc) { int ret_val; char message[100]; /* 警告 - 這是危險的,因為我們沒有告訴內核它允許寫多遠,因此它可能使緩沖區溢出。 * 在真正的產品程序中,我們應該使用兩個 ioctl - 一個告訴內核緩沖區長度而另一個填充緩沖區 */ ret_val = ioctl(file_desc, IOCTL_GET_MSG, message); if (ret_val < 0) { printf ("ioctl_get_msg failed:%d\n", ret_val); exit(-1); } printf("get_msg message:%s\n", message); } ioctl_get_nth_byte(int file_desc) { int i; char c; printf("get_nth_byte message:"); i = 0; while (c != 0) { c = ioctl(file_desc, IOCTL_GET_NTH_BYTE, i++); if (c < 0) { printf( "ioctl_get_nth_byte failed at the %d'th byte:\n", i); exit(-1); } putchar(c); } putchar('\n'); } /* 主函數 - 調用 ioctl 函數 */ main() { int file_desc, ret_val; char *msg = "Message passed by ioctl\n"; file_desc = open(DEVICE_FILE_NAME, 0); if (file_desc < 0) { printf ("Can't open device file: %s\n", DEVICE_FILE_NAME); exit(-1); } ioctl_get_nth_byte(file_desc); ioctl_get_msg(file_desc); ioctl_set_msg(file_desc, msg); close(file_desc); }