Linux4.x核心程式設計實戰——動態切庫(1)
接上一篇,我們首先來搭建開發環境。
很多發行版預設情況下已經安裝了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
#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一樣。
本期程式碼: