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

很多發行版預設情況下已經安裝了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++”。

Linux4.x核心程式設計實戰——動態切庫(1)

然後我們就可以新建一個目錄,並用vscode開啟,新建一個c程式檔案,此時vscode會自動生成一個。vscode目錄,裡面存放我們的配置檔案,為了能夠正確提示補全api,我們需要把核心標頭檔案所在的目錄給配置到

。vscode/c_cpp_properties。json

檔案中去,如下圖

Linux4.x核心程式設計實戰——動態切庫(1)

國際慣例,先來個hello world

/*

* hello。c

*/

#include

#include

static

int

__init

hello_init

void

){

printk

KERN_INFO

“hello!!!

\n

);

return

0

}

static

void

__exit

hello_exit

void

){

printk

KERN_INFO

“bye!!!

\n

);

}

// 開源協議申明,這裡只有有限的幾種協議可以選擇,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。h

stdlib。h

在內所有在

/usr/include

或者

/usr/local/include

等等。換句話說,我們只能使用

/usr/src/linux-header

裡面的api和庫,這就是為什麼我們沒有使用printf而是printk,因為printf在stdio。h裡面。

其次,要明確,我們在此並不是修改核心本身,而是去擴充套件核心,所以這裡主要研究的是核心模組程式設計,程式展示的就是核心模組的模板,不同於我們熟悉的使用者態程式設計,是從main函式為入口。核心模組只提供了兩個回撥點,

module_init

module_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

#include

#include

#include

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

=

‘\0’

//set EOF

printk

KERN_INFO

“[netcco]got:%s

\n

buf

);

return

len

}

static

int

__init

hello_init

void

){

proc_ops

owner

=

THIS_MODULE

proc_ops

write

=

proc_op_write

//設定寫函式到檔案操作

//建立 /proc/netcco 虛擬檔案,設定可讀寫許可權,繫結檔案操作

pde

=

proc_create

“netcco”

0666

NULL

&

proc_ops

);

if

pde

{

printk

KERN_ERR

“proc create failed!”

);

proc_remove

pde

);

return

ENOMEM

}

return

0

}

static

void

__exit

hello_exit

void

){

if

pde

){

proc_remove

pde

);

}

}

MODULE_LICENSE

“GPL”

);

MODULE_DESCRIPTION

“this is a helloworld test”

);

MODULE_AUTHOR

“Rowland”

);

module_init

hello_init

);

module_exit

hello_exit

);

然後,修改Makefile裡面的target,之前是hello。o現在改成netcco。o用同樣的方式安裝模組,然後直接

echo “hello world”>/proc/netcco

去核心日誌看效果

$> make

$> sudo insmod netcco。ko

$> echo “hello world”>/proc/netcco

$> tail /var/log/kern。log

Oct 13 15:18:10 Sjet kernel: [ 7758。007784] [netcco]got:hello world

Oct 13 15:18:10 Sjet kernel: [ 7758。007784]

相信大家看到這裡發現了一個問題,為什麼我在使用者態使用了echo寫入到/proc/netcco,而核心態的寫函式卻被呼叫了,難道不應該是,使用者態程式寫,核心態讀的過程嗎?我一開始也有這種思考,不過再仔細思考發現原來自己拿衣服。當我們呼叫echo往檔案裡面“寫”的時候本質上就是呼叫的這個核心的寫,也就是使用者態的write裡面會呼叫到file_operations。write一樣。

本期程式碼: