Linux : lkmpg 粗淺筆記 (1)

吳建興
50 min readSep 5, 2021

--

前言

因為我對 linux 實在太不熟了,於是參與了 Jserv 老師的課程 ,結果因為我太爛,導致每個作業都需要熬夜寫,還寫不完 XD 。 上班的時候,一不小心寫程式寫到睡著,看來我的意志力還是太薄弱了。實在是很佩服 Jserv 老師能夠花這麼多心力在研究以及教學上。

另一方面就是,我原本的實力太差了,導致上課的時候總是需要東補補西補補的 。 總之先把 linux kernel module programming guide 補完吧!好好的學習一下 linux kernel device driver。

恰好 Jserv 老師有維護一份可適用在新 kernel 下的 lkmpg 以及其範例,有興趣的人可以一起來欣賞 Jserv 老師與其學生們維護的 linux kernel module programming guide

我的筆記很可能會有錯誤,還請大家多多包涵。

Headers

想要編譯 linux kernel module 的話,要先裝一下 header 檔。或是勤勞一點,跟著鳥哥的文章:

一起抓 linux kernel 的 source code 把 linux kernel 給重新編譯一次。想要重新編譯的話,我推薦有一台或買一台專門拿來惡搞的電腦,不然就會像我一樣,一不小心把裝有作業的筆電給弄壞了,導致原本就常常熬夜的碩士生活變得更常熬夜。

The __init and __exit Macros

只要讓 function 用 __init 進行修飾,就可以讓 linux kernel 在執行完這個 function 後就釋放資源 。

__initdata 這個修飾詞也有類似的功效,在執行完 __init function 後也會釋放掉資源。

注意 __init 以及 __initdata 這兩個修飾詞只對 built-in driver 有效,對 loadable 是無效的。( only for built-in drivers, but not loadable modules )

static int hello3_data __initdata = 3;
static int __init hello_3_init(void)
{
pr_info("Hello, world %d\n", hello3_data);
return 0;
}

Passing Command Line Arguments to a Module

我們可以使用 command line 在 linux kernel module 在初始化時,傳遞參數給 module ,且參數是有型態的分別的。

可在 linux module 裡先設定好預設值,若使用者沒有對相對應的參數進行改變,就會使用預設值。
可用 module_param() 去定義一個參數的 type。
可用 module_param_array() 去定義一個使用陣列作為參數的 type。
可用 MODULE_PARM_DESC() 對參數下註解。

$ sudo insmod hello-5.ko mystring="supercalifragilisticexpialidocious" myintArray=-1,-1 
myshort is a short integer: 1
myint is an integer: 420
mylong is a long integer: 9999
mystring is a string: supercalifragilisticexpialidocious
myintArray[0] = -1
myintArray[1] = -1
got 2 arguments for myintArray.

由上述 command line 的使用方法可以得知,陣列的多個元素是用 ‘,’ 去隔開的。

Modules Spanning Multiple Files

單一一個 kernel module 可以是多個 source file 編譯而成的。假設我們將一個 module 拆分成 part1.c, part2.c ,則 Makefile 可以寫成下面這樣。

test-objs := part1.o part2.o 

all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

Building modules for a precompiled kernel

假如 kernel 不是自己手動從頭編譯的話,可能會遇上些問題。編譯的版本號可能會跟自己正在運行的 kernel 對不上,而產生一些問題。這個章節就在教我們,怎麼樣解決這樣的問題。
但因為我沒碰上這個問題,就沒操作這一章節的練習了( 我好懶 QQ )。

How modules begin and end

每個 kernel module 都會有個 init_module 來進行初始化設定,以及 cleanup_module 來撤銷 init_module 所做的設定。

Functions available to modules

linux kernel module 不像 user space 那麼方便,有偉大的 glibc 可以使用,但我們仍舊可以使用 linux kernel 所 export 的符號 ( symbol )。想知道自己的 linux kernel 有 export 了哪些符號,可看 /proc/kallsyms。。。。還真是多啊。

$ cat /proc/kallsyms
.
.
0000000000000000 r __kstrtabns_sdio_get_host_pm_caps
0000000000000000 r __kstrtabns_sdio_memcpy_fromio
0000000000000000 r __kstrtabns_sdio_memcpy_toio
0000000000000000 r __kstrtabns_sdio_readb
0000000000000000 r __kstrtabns_sdio_readl
0000000000000000 r __kstrtabns_sdio_readsb
0000000000000000 r __kstrtabns_sdio_readw
0000000000000000 r __kstrtabns_sdio_register_driver
.
.

library function 跟 system call 是不同的,用 strace 就可以觀察一個 library function 用了哪些 system call

#include <stdio.h> 

int main(void)
{
printf("hello");
return 0;
}
---
$ gcc -Wall -o hello hello.c
---
$ strace ./hello
execve("./hello", ["./hello"], 0x7ffefc86ccf0 /* 24 vars */) = 0
brk(NULL) = 0x561c68cf0000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=105219, ...}) = 0
mmap(NULL, 105219, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fd77068e000
close(3) = 0
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\20\35\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2030928, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd77068c000
mmap(NULL, 4131552, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fd77008e000
mprotect(0x7fd770275000, 2097152, PROT_NONE) = 0
mmap(0x7fd770475000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e7000) = 0x7fd770475000
mmap(0x7fd77047b000, 15072, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fd77047b000
close(3) = 0
arch_prctl(ARCH_SET_FS, 0x7fd77068d4c0) = 0
mprotect(0x7fd770475000, 16384, PROT_READ) = 0
mprotect(0x561c68047000, 4096, PROT_READ) = 0
mprotect(0x7fd7706a8000, 4096, PROT_READ) = 0
munmap(0x7fd77068e000, 105219) = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
brk(NULL) = 0x561c68cf0000
brk(0x561c68d11000) = 0x561c68d11000
write(1, "hello", 5hello) = 5
exit_group(0) = ?
+++ exited with 0 +++

可以看下 write(1, “hello”, 5hello) ,這應該是真正將字元輸出到螢幕上的 system call。

Device Drivers

major number 代表被哪個 driver 給控制。
相同的 major number ,不同的 minor number ,通常代表被同一個 driver 控制的不同硬體。
TODO : 有沒有任何範例?

character device 跟 block device 最大的差異在於, block device 通常會有一個 buffer , 並且輸入與輸出會是以一個 block size 為單位。
TODO : 有沒有任何範例?

我們可以使用 mknod 來建立一個節點,下面的範例就是在 /dev/coffee 建立一個 character device 節點。 major number == 12, minor number == 2。

$ mknod /dev/coffee c 12 2

Linux Kernel 只在意該節點的 major number , 而 minor number 則是 Driver 自己要想辦法處理的。
TODO : 有沒有任何範例?

The file structure

這裡的 file structure 指的並不是 glibc 實作出的 FILE , 也不是真的在硬碟裡的那種 file 。 這裡的 file 指的是在 linux kernel 裡以 inode 的形式所表現的抽象的 file 。

Registering A Device

major number 可以使我們知道,哪一個 driver 負責哪一個 device file , 而 minor number 則是讓 driver 自己辨別不同的 device 。

可使用 register_chrdev 去註冊一個 character device。可以注意到,我們並不需要傳入 minor number , 因為 linux kernel 也不在意 minor number , 只有我們的 driver 本身會使用到 minor number 。

// major : 註冊者想要使用的 major number
// name : 該 device 會出現在 /proc/devices 的名字
// fops : 指向 file_operation 的指標。
// return 值若是負的,就是我們註冊失敗了。
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);

但是我們怎麼知道哪個 major number 沒人使用呢? 其實可以向 linux kernel 動態要求一個空的 major number 。

只要當我們使用 register_chrdev 時, major 這個參數為 0 , 那 register_chrdev 的回傳值就是我們拿到的 major number 。

使用動態拿取 major number 的話,我們就不能事先產生 device file 了。但我們仍舊能:
1. driver 可以將拿到的 major number 打印出來,我們再手動建立 device file。
2. 新建立好的 device 會出現在 /proc/devices,我們可以讀取該檔案後,手動或用 script 建立 device file 。
TODO : 有任何範例嗎?
3. 我們可以在 driver 內部,使用 register_chrdev 註冊完 character device 後,再用 drvice_create 建立 device file 。 並可在 cleanup_module 時用 device_destroy 來刪除該 device file。

Q: 使用 register_chrdev ,使的 /proc/devices 裡產生一的相對應的檔案。該檔案跟我用 device_create 創建出的 device file 有何差別呢?
A: 我在耍笨...完全會錯意了。/proc/devices 並不是一個資料夾,當我們註冊 character device 時,若我們 cat /proc/devices , 就能看到一個 driver 列表。
$ cat /proc/devices
Character devices:
1 mem
4 /dev/vc/0
4 tty
4 ttyS
5 /dev/tty
5 /dev/console
5 /dev/ptmx
5 ttyprintk
6 lp
7 vcs
10 misc
13 input
21 sg
29 fb
81 video4linux
89 i2c
99 ppdev
108 ppp
116 alsa
.
.
.
Block devices:
7 loop
8 sd
9 md
11 sr
65 sd
66 sd
67 sd
68 sd
69 sd
70 sd
71 sd
128 sd
129 sd
130 sd

Unregistering A Device

kernel module 是不能隨隨便便就 unregister 的。假如有個 process 正在使用該 kernel module 的 device file ,用到一半時我們就將該 kernel module 給 unregister , 那後續的行為是無法預期的 ( 通常不會是個好結果就是了 )。

但 linux kernel 很貼心地有個計數器,會幫我們計算還有多少 process 正在使用我們的 kernel module 。

範例可看下面的 "Used by" 數目。

$ lsmod
Module Size Used by
xt_conntrack 16384 2
xt_MASQUERADE 20480 2
nf_conntrack_netlink 49152 0
nfnetlink 16384 2 nf_conntrack_netlink
xfrm_user 36864 1
xfrm_algo 16384 1 xfrm_user

chardev.c

現在來看一個 character device 的範例:

/*
* chardev.c: Creates a read-only char device that says how many times
* you have read from the dev file
*/
// 定義當別人對我們的 file descriptor 使用 read 系統呼叫,我們要執行哪個相對應的 function 。
static struct file_operations chardev_fops = {
.read = device_read,
.write = device_write,
.open = device_open,
.release = device_release,
};
///////////////////////////////////////////static int __init chardev_init(void)
{
// 註冊字元裝置
major = register_chrdev(0, DEVICE_NAME, &chardev_fops);
if (major < 0) {
pr_alert("Registering char device failed with %d\n", major);
return major;
}
pr_info("I was assigned major number %d.\n", major); // 註冊 class
cls = class_create(THIS_MODULE, DEVICE_NAME);
// 在 /dev 下建立 device file 。
// 完成這一步,才會在 /dev/ 裡面生成 device file ,供我們用 fops 來進行操作。
device_create(cls, NULL, MKDEV(major, 0), NULL, DEVICE_NAME);
pr_info("Device created on /dev/%s\n", DEVICE_NAME); return SUCCESS;
}
///////////////////////////////////////////static void __exit chardev_exit(void)
{
// 撤銷 device file
device_destroy(cls, MKDEV(major, 0));
// 撤銷 class
class_destroy(cls);
// 撤銷字元裝置的註冊
/* Unregister the device */
unregister_chrdev(major, DEVICE_NAME);
}
////////////////////////////////////////////* Methods *//* Called when a process tries to open the device file, like
* "sudo cat /dev/chardev"
*/
static int device_open(struct inode *inode, struct file *file)
{
static int counter = 0;
// 避免重複開啟
if (open_device_cnt)
return -EBUSY;
open_device_cnt++;
sprintf(msg, "I already told you %d times Hello world!\n", counter++);
msg_ptr = msg;
// linux kernel 會幫我們將計數器 +1 , 避免還有 process 使用這個 driver 的時候,就將此 driver 撤銷。
try_module_get(THIS_MODULE);
return SUCCESS;
}
////////////////////////////////////////////* Called when a process closes the device file. */
static int device_release(struct inode *inode, struct file *file)
{
open_device_cnt--; /* We're now ready for our next caller */

// 表示又少一個 process 正在使用此 module , 當沒有 process 在使用此 driver 時,此 driver 才能真正地釋放資源
/* Decrement the usage count, or else once you opened the file, you will
* never get get rid of the module.
*/
module_put(THIS_MODULE);
return SUCCESS;
}
////////////////////////////////////////////* Called when a process, which already opened the dev file, attempts to
* read from it.
*/
static ssize_t device_read(struct file *filp, /* see include/linux/fs.h */
char *buffer, /* buffer to fill with data */
size_t length, /* length of the buffer */
loff_t *offset)
{
/* Number of bytes actually written to the buffer */
int bytes_read = 0;
/* If we are at the end of message, return 0 signifying end of file. */
if (*msg_ptr == 0)
return 0;
/* Actually put the data into the buffer */
while (length && *msg_ptr) {
/* The buffer is in the user data segment, not the kernel
* segment so "*" assignment won't work. We have to use
* put_user which copies data from the kernel data segment to
* the user data segment.
*/
put_user(*(msg_ptr++), buffer++);
length--;
bytes_read++;
}
/* Most read functions return the number of bytes put into the buffer. */
return bytes_read;
}
////////////////////////////////////////////* Called when a process writes to dev file: echo "hi" > /dev/hello */
static ssize_t device_write(struct file *filp,
const char *buff,
size_t len,
loff_t *off)
{
pr_alert("Sorry, this operation is not supported.\n");
return -EINVAL;
}
///////////////////////////////////////////module_init(chardev_init);
module_exit(chardev_exit);
MODULE_LICENSE("GPL");

The /proc File System

/proc 檔案系統原本是用來跟 process 溝通用的 ( 也是它名稱的由來 ),但是久而久之變成取得有關 kernel 資訊的地方了。

例如 /proc/modules 可以取得與 kernel module 有關的資訊。

$ cat /proc/modules
xt_conntrack 16384 2 - Live 0x0000000000000000
xt_MASQUERADE 20480 2 - Live 0x0000000000000000
nf_conntrack_netlink 49152 0 - Live 0x0000000000000000
nfnetlink 16384 2 nf_conntrack_netlink, Live 0x0000000000000000
xfrm_user 36864 1 - Live 0x0000000000000000
xfrm_algo 16384 1 xfrm_user, Live 0x0000000000000000
xt_addrtype 16384 2 - Live 0x0000000000000000
iptable_filter 16384 1 - Live 0x0000000000000000
iptable_nat 16384 1 - Live 0x0000000000000000
nf_nat 45056 2 xt_MASQUERADE,iptable_nat, Live 0x0000000000000000
nf_conntrack 143360 4 xt_conntrack,xt_MASQUERADE,nf_conntrack_netlink,nf_nat, Live 0x0000000000000000
nf_defrag_ipv6 24576 1 nf_conntrack, Live 0x0000000000000000
.
.
.

例如 /proc/meminfo 可以取得與 memory usage 有關的資訊。

$ cat /proc/meminfo
MemTotal: 12152288 kB
MemFree: 9810300 kB
MemAvailable: 11285872 kB
Buffers: 48696 kB
Cached: 1709356 kB
SwapCached: 0 kB
Active: 909788 kB
Inactive: 1112324 kB
Active(anon): 302760 kB
Inactive(anon): 41876 kB
Active(file): 607028 kB
Inactive(file): 1070448 kB
Unevictable: 42016 kB
.
.
.

/proc 是一個僅存在於 memory 裡的檔案系統,一般的檔案系統是在 disk 上的。
在一般的檔案系統下, inode number 是一個指向 disk 中,index-node ( 簡稱 inode ) 所在的地方。 inode 包含了有關檔案的一些資訊,例如檔案的 permission , 或檔案的位置等等。

以下是使用 /proc 檔案系統的簡單範例。在 init_module 時創建 /proc/helloworld, 並在 cleanup_module 的時候,把 /proc/helloworld 撤銷。

/*
* procfs1.c
*/
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/uaccess.h>
#include <linux/version.h>
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 6, 0)
#define HAVE_PROC_OPS
#endif
#define procfs_name "helloworld"struct proc_dir_entry *Our_Proc_File;ssize_t procfile_read(struct file *filePointer,
char *buffer,
size_t buffer_length,
loff_t *offset)
{
char s[13] = "HelloWorld!\n";
int len = sizeof(s);
ssize_t ret = len;
// 使用 copy_to_user , 將資料從 kernel space 複製到 user space
if (*offset >= len || copy_to_user(buffer, s, len)) {
pr_info("copy_to_user failed\n");
ret = 0;
} else {
pr_info("procfile read %s\n", filePointer->f_path.dentry->d_name.name);
*offset += len;
}
return ret;
}
#ifdef HAVE_PROC_OPS
static const struct proc_ops proc_file_fops = {
.proc_read = procfile_read,
};
#else
static const struct file_operations proc_file_fops = {
.read = procfile_read,
};
#endif
static int __init procfs1_init(void)
{
// 建立一個 /proc/* 檔案,該檔的操作為 proc_file_fops 所定義
Our_Proc_File = proc_create(procfs_name, 0644, NULL, &proc_file_fops);
if (NULL == Our_Proc_File) {
proc_remove(Our_Proc_File);
pr_alert("Error:Could not initialize /proc/%s\n", procfs_name);
return -ENOMEM;
}
pr_info("/proc/%s created\n", procfs_name);
return 0;
}
static void __exit procfs1_exit(void)
{
// 使用 proc_remove 刪除該檔案
proc_remove(Our_Proc_File);
pr_info("/proc/%s removed\n", procfs_name);
}
module_init(procfs1_init);
module_exit(procfs1_exit);
MODULE_LICENSE("GPL");

Read and Write a /proc File

/proc 除了讀取以外,當然也可以進行寫入。
當 user space 對 kernel module 進行 read ( 也就是 kernel module 要輸出資料給 user space) , kernel module 需要使用 “copy_to_user” or “put_user” 將資料從 kernel space 複製到 user space 。

當 user space 想要對 kernel module 進行write , 需要使用 “copy_from_user” or “get_user” 將資料從 user space 複製到 kernel space。

Manage /proc file with seq_file

TODO : seq_file 感覺是個大項目,另外再寫筆記。

sysfs: Interacting with your module

/*
* hello-sysfs.c sysfs example
*/
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/kobject.h>
#include <linux/module.h>
#include <linux/string.h>
#include <linux/sysfs.h>
static struct kobject *mymodule;/* the variable you want to be able to change */
static int myvariable = 0;
// 當 user space 想要讀取 /sys/kernel/mymodule/myvariable 時,要做什麼事情
static ssize_t myvariable_show(struct kobject *kobj,
struct kobj_attribute *attr,
char *buf)
{
return sprintf(buf, "fdgk : %d\n", myvariable);
}
// 當對 myvariable 進行寫入時,要做什麼動作
static ssize_t myvariable_store(struct kobject *kobj,
struct kobj_attribute *attr,
char *buf,
size_t count)
{
sscanf(buf, "%du", &myvariable);
return count;
}
// 設定 myvariable 的屬性
static struct kobj_attribute myvariable_attribute =
__ATTR(myvariable, 0660, myvariable_show, (void *) myvariable_store);
static int __init mymodule_init(void)
{
int error = 0;
pr_info("mymodule: initialised\n"); // 在 /sys/kernel/ 裡建立一個資料夾 "mymodule"
mymodule = kobject_create_and_add("mymodule", kernel_kobj);
if (!mymodule)
return -ENOMEM;
// 在 /sys/kernel/mymodule 裡建立一個檔案 "myvariable"
error = sysfs_create_file(mymodule, &myvariable_attribute.attr);
if (error) {
pr_info(
"failed to create the myvariable file "
"in /sys/kernel/mymodule\n");
}
return error;
}
static void __exit mymodule_exit(void)
{
pr_info("mymodule: Exit success\n");
kobject_put(mymodule);
}
module_init(mymodule_init);
module_exit(mymodule_exit);
MODULE_LICENSE("GPL");// TODO :
// struct kobject
// struct kobj_attribute
// __ATTR
// kobject_create_and_add
// sysfs_create_file

當 module 用 insmod 裝起來後,就可以去讀取或寫入 /sys/kernel/mymodule/myvariable 這個檔案了。 user space 可以藉由 myvariable 這個檔案,與 hello-sysfs.ko 這個 kernel module 進行互動。這邊的互動就是去操控 myvariable 這個變數的值。

$ sudo insmod hello-sysfs.ko
$ sudo cat /sys/kernel/mymodule/myvariable
fdgk : 0
$ echo "32" | sudo tee /sys/kernel/mymodule/myvariable
$ sudo cat /sys/kernel/mymodule/myvariable
fdgk : 32
$ sudo rmmod hello_sysfs

Talking To Device Files

_IOW, _IOR, _IOWR 的定義可以看 "/include/uapi/asm-generic/ioctl.h",這個 macro 會將參數轉為一個數字,所以在 chardev2.c 才能使用 swtch 去判別,現在 user space 是想要做哪個動作。

/*
* chardev.h - the header file with the ioctl definitions.
*
* The declarations here have to be in a header file, because they need
* to be known both to the kernel module (in chardev.c) and the process
* calling ioctl (ioctl.c).
*/
#ifndef CHARDEV_H
#define CHARDEV_H
#include <linux/ioctl.h>/* The major device number. We can not rely on dynamic registration
* any more, because ioctls need to know it.
*/
#define MAJOR_NUM 100
/* Set the message of the device driver */
#define IOCTL_SET_MSG _IOW(MAJOR_NUM, 0, char *)
/* _IOW means that we are creating an ioctl command number for passing
* information from a user process to the kernel module.
*
* The first arguments, MAJOR_NUM, is the major device number we are using.
*
* The second argument is the number of the command (there could be several
* with different meanings).
*
* The third argument is the type we want to get from the process to the
* kernel.
*/
/* Get the message of the device driver */
#define IOCTL_GET_MSG _IOR(MAJOR_NUM, 1, char *)
/* This IOCTL is used for output, to get the message of the device driver.
* However, we still need the buffer to place the message in to be input,
* as it is allocated by the process.
*/
/* Get the n'th byte of the message */
#define IOCTL_GET_NTH_BYTE _IOWR(MAJOR_NUM, 2, int)
/* The IOCTL is used for both input and output. It receives from the user
* a number, n, and returns Message[n].
*/
/* The name of the device file */
#define DEVICE_FILE_NAME "char_dev"
#endif
--------
/*
* chardev2.c - Create an input/output character device
*/
#include <linux/cdev.h>
#include <linux/delay.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/irq.h>
#include <linux/kernel.h> /* We are doing kernel work */
#include <linux/module.h> /* Specifically, a module */
#include <linux/poll.h>
#include "chardev.h"
#define SUCCESS 0
#define DEVICE_NAME "char_dev"
#define BUF_LEN 80
/*
因為我們不希望同時讓兩個 process 來打開這個 driver
所以這邊用一個簡單的計數器來計算,現在有多少個 driver 被打開了
*/
static int Device_Open = 0;
/*
The message the device will give when asked
*/
static char Message[BUF_LEN];
/*
用一個指標來標示我們現在讀到 Message 的位置
假如 Message 的長度大於 buffer,我們也能從上次讀取的位置來繼續往下讀
*/
static char *Message_Ptr;
static struct class *cls;/* This is called whenever a process attempts to open the device file */
static int device_open(struct inode *inode, struct file *file)
{
pr_info("device_open(%p)\n", file);
/* 不希望同時有兩個以上的 process 打開這個 driver */
if (Device_Open)
return -EBUSY;
Device_Open++;
/* Initialize the message */
Message_Ptr = Message;
try_module_get(THIS_MODULE);
return SUCCESS;
}
static int device_release(struct inode *inode, struct file *file)
{
pr_info("device_release(%p,%p)\n", inode, file);
/* We're now ready for our next caller */
Device_Open--;
module_put(THIS_MODULE);
return SUCCESS;
}
/* This function is called whenever a process which has already opened the
* device file attempts to read from it.
*/
static ssize_t device_read(struct file *file, /* see include/linux/fs.h */
char __user *buffer, /* buffer to be filled */
size_t length, /* length of the buffer */
loff_t *offset)
{
/* Number of bytes actually written to the buffer */
int bytes_read = 0;
pr_info("device_read(%p,%p,%ld)\n", file, buffer, length); /* If at the end of message, return 0 (which signifies end of file). */
if (*Message_Ptr == 0)
return 0;
/* Actually put the data into the buffer */
while (length && *Message_Ptr) {
/* Because the buffer is in the user data segment, not the kernel
* data segment, assignment would not work. Instead, we have to
* use put_user which copies data from the kernel data segment to
* the user data segment.
*/
put_user(*(Message_Ptr++), buffer++);
length--;
bytes_read++;
}
pr_info("Read %d bytes, %ld left\n", bytes_read, length); /* Read functions are supposed to return the number of bytes actually
* inserted into the buffer.
*/
return bytes_read;
}
/* called when somebody tries to write into our device file. */
static ssize_t device_write(struct file *file,
const char __user *buffer,
size_t length,
loff_t *offset)
{
int i;
pr_info("device_write(%p,%s,%ld)", file, buffer, length); for (i = 0; i < length && i < BUF_LEN; i++)
get_user(Message[i], buffer + i);
Message_Ptr = Message; /* Again, return the number of input characters used. */
return i;
}
/* This function is called whenever a process tries to do an ioctl on our
* device file. We get two extra parameters (additional to the inode and file
* structures, which all device functions get): the number of the ioctl called
* and the parameter given to the ioctl function.
*
* If the ioctl is write or read/write (meaning output is returned to the
* calling process), the ioctl call returns the output of this function.
*/
long device_ioctl(struct file *file, /* ditto */
unsigned int ioctl_num, /* number and param for ioctl */
unsigned long ioctl_param)
{
int i;
char *temp;
char ch;
/* Switch according to the ioctl called */
switch (ioctl_num) {
case IOCTL_SET_MSG:
/* Receive a pointer to a message (in user space) and set that to
* be the device's message. Get the parameter given to ioctl by
* the process.
*/
temp = (char *) ioctl_param;
/* Find the length of the message */
get_user(ch, temp);
for (i = 0; ch && i < BUF_LEN; i++, temp++)
get_user(ch, temp);
device_write(file, (char *) ioctl_param, i, 0);
break;
case IOCTL_GET_MSG:
/* Give the current message to the calling process - the parameter
* we got is a pointer, fill it.
*/
i = device_read(file, (char *) ioctl_param, 99, 0);
/* Put a zero at the end of the buffer, so it will be properly
* terminated.
*/
put_user('\0', (char *) ioctl_param + i);
break;
case IOCTL_GET_NTH_BYTE:
/* This ioctl is both input (ioctl_param) and output (the return
* value of this function).
*/
return Message[ioctl_param];
break;
}
return SUCCESS;
}
/* Module Declarations *//* This structure will hold the functions to be called when a process does
* something to the device we created. Since a pointer to this structure
* is kept in the devices table, it can't be local to init_module. NULL is
* for unimplemented functions.
*/
struct file_operations Fops = {
.read = device_read,
.write = device_write,
.unlocked_ioctl = device_ioctl,
.open = device_open,
.release = device_release, /* a.k.a. close */
};
/* Initialize the module - Register the character device */
static int __init chardev2_init(void)
{
/* Register the character device (atleast try) */
int ret_val = register_chrdev(MAJOR_NUM, DEVICE_NAME, &Fops);
/* Negative values signify an error */
if (ret_val < 0) {
pr_alert("%s failed with %d\n",
"Sorry, registering the character device ", ret_val);
return ret_val;
}
cls = class_create(THIS_MODULE, DEVICE_FILE_NAME);
device_create(cls, NULL, MKDEV(MAJOR_NUM, 0), NULL, DEVICE_FILE_NAME);
pr_info("Device created on /dev/%s\n", DEVICE_FILE_NAME); return 0;
}
/* Cleanup - unregister the appropriate file from /proc */
static void __exit chardev2_exit(void)
{
device_destroy(cls, MKDEV(MAJOR_NUM, 0));
class_destroy(cls);
/* Unregister the device */
unregister_chrdev(MAJOR_NUM, DEVICE_NAME);
}
module_init(chardev2_init);
module_exit(chardev2_exit);
MODULE_LICENSE("GPL");

上面是 kernel space 的程式碼範例,接下來是 user space 的程式碼。簡單地把一個字串從 user space 丟給 kernel space , 之後再從 kernel space 丟一個字串給 user space。

/*
* fdgk_chr.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include "chardev.h"
int main(int argc, char *argv[]) { // 首先打開 driver 用 device_create 所創建出來的 device file
int fd = open("/dev/char_dev", O_RDONLY);
if(fd == -1) {
printf("I cannot open it QQ\n");
exit(-1);
}
// 想要丟給 device driver 的 message
char *message = "fdgk hihi!\n";
// 想要從 kernel 拿取的 message
char message_get_from_kernel[256];
// 使用 ioctl 將 message 丟給 kernel
ioctl(fd, IOCTL_SET_MSG, message);
// 使用 ioctl 從 kernel 拿取 message
ioctl(fd, IOCTL_GET_MSG, message_get_from_kernel);
// 將從 kernel 拿取的 message 給列印出來
printf("message_get_from_kernel : %s\n", message_get_from_kernel);
close(fd);
return 0;
}

--

--

吳建興
吳建興

Written by 吳建興

I want to be a good programmer.(´・ω・`)

Responses (1)