接上一篇,我們首先來搭建開發環境。

很多發行版默認情況下已經安裝了linux-header頭文件,意味著它可以直接來開發內核了。當然也有可能沒有安裝。

可以使用先使用uname -r來查看當前使用的內核版本,然後到/usr/src/下看是否存在這個內核目錄,如果存在再進一步確認裡面的/usr/src/linux-headers-xxx.xx/include/linux裡面是否真的有大量頭文件。如果存在那麼我們的開發環境就沒有問題。

當然各個發行版的包管理工具都可以一句話安裝。首先使用包管理工具查詢關鍵詞「linux-header」,「kernel-devel」,「linux-devel」等等關鍵詞,然後安裝和自己uname看到的對應的版本就好了,比如ubuntu。

sudo apt-get install linux-headers-4.15.0-generic

其他的發行版都是大同小異,就不贅述了。

然後我選擇使用vscode作為開發ide,安裝擴展「c/c++」。

然後我們就可以新建一個目錄,並用vscode打開,新建一個c程序文件,此時vscode會自動生成一個.vscode目錄,裡面存放我們的配置文件,為了能夠正確提示補全api,我們需要把內核頭文件所在的目錄給配置到 .vscode/c_cpp_properties.json 文件中去,如下圖

國際慣例,先來個hello world

/*
* hello.c
*/
#include <linux/kernel.h>
#include <linux/module.h>

static int __init hello_init(void){
printk(KERN_INFO "hello!!!
");
return 0;
}

static void __exit hello_exit(void){
printk(KERN_INFO "bye!!!
");
}

// 開源協議申明,這裡只有有限的幾種協議可以選擇,GPL和GPL的變種
// 這下知道為什麼nvidia不願意好好寫驅動了吧
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("this is a helloworld test");
MODULE_AUTHOR("Rowland");

module_init(hello_init);
module_exit(hello_exit);

首先我們不是在用戶態下面編程,意味著我們熟悉的glibc庫就使用不了,包括stdio.hstdlib.h在內所有在/usr/include或者/usr/local/include等等。換句話說,我們只能使用/usr/src/linux-header裡面的api和庫,這就是為什麼我們沒有使用printf而是printk,因為printf在stdio.h裡面。

其次,要明確,我們在此並不是修改內核本身,而是去擴展內核,所以這裡主要研究的是內核模塊編程,程序展示的就是內核模塊的模板,不同於我們熟悉的用戶態編程,是從main函數為入口。內核模塊只提供了兩個回調點,module_initmodule_exit 用來安裝和卸載我們的模塊,意味著我們通常也不會在內核模塊裡面寫類似我們在用戶態那種一個循環來服務某種業務的功能。而是通過初始化函數把我們的功能安裝到某種事件上面去,等著內核來回調我們的函數。

最後就是編譯模塊,是通過Makefile來進行的

#
# Makefile 注意文件名大小寫
#
obj-m += hello.o # 注意這裡後綴是.o 不是.c

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

格式比較固定,看得出來,這個Makefile去調用了內核目錄裡面的Makefile,那裡才是真正編譯的Makefile。所以我們這個Makefile就是個萬金油,不管之後寫什麼模塊都可以用這一套。

完成了這一切之後,打開終端,使用make編譯。會生成一大堆東西,不管他們,重點找到一個.ko文件,沒錯這就是我們的模塊了。然後使用 sudo insmod hello.ko就可以安裝模塊了,這個printk列印的內容也沒有辦法直接顯示到我們當前的終端,所以我們需要去內核日誌裡面去查看,它位於 /var/log/kern.log,然後是 sudo rmmod hello 卸載模塊,下載的時候可以不加後綴.ko

$> sudo insmod hello.ko
$> tail /var/log/kern.log
Oct 13 14:38:55 Sjet kernel: [ 5403.775181] hello!!!
$>
$> sudo rmmod hello
$> tail /var/log/kern.log
Oct 13 14:38:55 Sjet kernel: [ 5403.775181] hello!!!
Oct 13 14:40:28 Sjet kernel: [ 5496.372860] bye!!!

好了,接下來就要進入正題了,首先,我們要為自己的項目命名,一個狂拽炫酷的名字——netcco,net首席渠道官,是不是很貼切呢!

第一個要懟的要點就是用戶態和內核態的交互。這裡就需要用到虛擬文件系統和內存拷貝等技術。比如我們要查看cpu信息。

$> cat /proc/cpuinfo

這裡就是一個典型,proc就是屬於內核的虛擬文件系統。用戶程序通過對這個文件讀寫來達到跟內核交互的目的。相信此時你對「萬物皆文件」也有了更深的感悟。

既然是文件,當然逃不掉讀寫操作等等等等。在內核態,專門有一種數據結構來描述文件操作——struct file_operations 他位於頭文件linux/fs.h 感興趣的話可以去看看原型

struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
unsigned long mmap_supported_flags;
…………

我只截取了一半不到。天!!!它實在是太長了,難道我們要實現一種fd供用戶程序都寫要寫這麼多操作?當然不用,我們只需要設置讀寫 read和write函數就好了。

於是

/*
* netcco.c
*/
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/uaccess.h>

static char buf[1024];
static struct proc_dir_entry *pde; //虛擬文件位於 /proc/xxx
static struct file_operations proc_ops; //文件操作,綁定到虛擬文件上

static ssize_t proc_op_write(struct file *filp,
const char __user *buffer,
size_t len,
loff_t* offset)
{
if(copy_from_user(buf, buffer, len)){
//注意copy_from_user就是從用戶態拷貝,buffer就是指向用戶態數據的指針
return -ENOMEM;
}
buf[len]=

相关文章