内核必须懂(一): 用系统调用打印Hello, world!
内核必须懂(二): 文件系统初探
内核必须懂(三): 重编Ubuntu18.04LTS内核4.15.0
内核必须懂(四): 撰写内核驱动
内核必须懂(五): per-CPU变量
前言
之前的文章里面说了简单的.ko文件编译. 这里继续深入下去. 当然, 还是从驱动的Hello, world!开始.
驱动模块里的Hello, world!
首先是源码部分, 这里由于是内核, 所以c库的函数就不能用了, 比如printf这样的, 要用printk替代, 这里的k就是指kernel.
然后__init和__exit意味着只有初始化和卸载才会执行函数, 也就是都只执行一次.
module_init和module_exit理解为注册函数就行了.
#include<linux/kernel.h>
#include<linux/init.h>
#include<linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Sean Depp");
static int __init hello_init(void)
{
printk("Hello, sean!\n") ;
return 0;
}
static void __exit hello_exit(void)
{
printk("Exit, sean!\n");
}
module_init(hello_init);
module_exit(hello_exit);
Makefile常规写法就好, 没什么特别要说的. 当然, 你可以写的更有效一些, 比如编译完成之后删除除了.ko文件之外的其它生成文件. 下面给出常规写法和改进写法:
obj-m:=helloKo.o
PWD:=$(shell pwd)
KER_DIR=/lib/modules/$(shell uname -r)/build
all :
make -C $(KER_DIR) M=$(PWD) modules
clean :
make -C $(KER_DIR) M=$(PWD) clean
ifneq ($(KERNELRELEASE),)
obj-m := helloKo.o
else
PWD := $(shell pwd)
KER_DIR ?= /lib/modules/$(shell uname -r)/build
default:
$(MAKE) -C $(KER_DIR) M=$(PWD) modules
rm *.order *.symvers *.mod.c *.o .*.o.cmd .*.cmd .tmp_versions -rf
endif
来编译生成模块, 之后安装和卸载.
sudo make
sudo insmod helloKo.ko
sudo rmmod helloKo
安装与卸载
我想你看到了一个提示Makefile:934: "Cannot use CONFIG_STACK_VALIDATION=y, please install libelf-dev, libelf-devel or elfutils-libelf-devel", 很明显这是一个内核编译的参数没生效, 但是编译成功了. 于是我好奇就装了一下libelf-dev, 反而就无法编译成功了. 这里如果有大佬可以告知我为什么, 评论区见, 提前笔芯. 所以这里暂时不管这个参数了.看到了警告, 这是缺了个库, 最直接的解决方案就是, 安装这个库之后, 重编译内核. 否则其他方案都过于麻烦.
安装与卸载当然, 可以用改进的Makefile再操作一次, 这次用lsmod查看一下安装的模块, 用dmesg查看信息是否打印出来.
lsmod dmesg成功看到模块和打印的消息:
自定义设备驱动
接下来更进一步, 写一下驱动代码, 这里可以自定义驱动的open, ioctl等等函数. 这里的MAJOR_NUM和DEVICE_NAME宏要记一下, 一个是设备节点号, 一个是设备名称.
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#define MAJOR_NUM 231
#define DEVICE_NAME "hellodr"
int DriverOpen( struct inode *pslINode, struct file *pslFileStruct )
{
printk( KERN_ALERT DEVICE_NAME " hello open.\n" );
return(0);
}
ssize_t DriverWrite( struct file *pslFileStruct, const char __user *pBuffer, size_t nCount, loff_t *pOffset )
{
printk( KERN_ALERT DEVICE_NAME " hello write.\n" );
return(0);
}
long DriverIOControl( struct file *pslFileStruct, unsigned int uiCmd, unsigned long ulArg )
{
printk( KERN_ALERT DEVICE_NAME " hello ioctl.\n" );
return(0);
}
struct file_operations hello_flops = {
.owner = THIS_MODULE,
.open = DriverOpen,
.write = DriverWrite,
.unlocked_ioctl = DriverIOControl
};
static int __init hello_init( void )
{
int ret;
ret = register_chrdev( MAJOR_NUM, DEVICE_NAME, &hello_flops );
if ( ret < 0 )
{
printk( KERN_ALERT DEVICE_NAME " can't register major number.\n" );
return(ret);
}
printk( KERN_ALERT DEVICE_NAME " initialized.\n" );
return(0);
}
static void __exit hello_exit( void )
{
printk( KERN_ALERT DEVICE_NAME " removed.\n" );
unregister_chrdev( MAJOR_NUM, DEVICE_NAME );
}
module_init( hello_init );
module_exit( hello_exit );
MODULE_LICENSE( "GPL" );
MODULE_AUTHOR( "Sean Depp" );
用户态方面, 写个调用open和ioctl函数的.
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <iostream>
#include <sys/types.h>
/*提供类型pid_t,size_t的定义*/
#include <sys/stat.h>
#include <sys/ioctl.h>
/* BSD and Linux */
#include <stropts.h>
/* XSI STREAMS */
#include <string.h>
using namespace std;
int main( void )
{
int fd;
if ( (fd = open( "/dev/hellodr", O_RDWR ) ) < 0 )
{
cerr << strerror( errno ) << endl;
return(-1);
}
ioctl( fd, 1, 0 );
close( fd );
return(0);
}
Makefile文件也是相似的.
ifneq ($(KERNELRELEASE),)
obj-m := helloDr.o
else
PWD := $(shell pwd)
KER_DIR ?= /lib/modules/$(shell uname -r)/build
default:
$(MAKE) -C $(KER_DIR) M=$(PWD) modules
rm *.order *.symvers *.mod.c *.o .*.o.cmd .*.cmd .tmp_versions -rf
endif
lsmod 设备信息用g++和make编译一下文件, 来跑下. 如果你直接跑是不行的, 需要链接节点. 从lsmod打印的信息来看, 已经成功安装模块了. 然后你可以查看/proc/devices中, 也出现了设备名和设备号:
运行 dmesg所以需要链接它们, 之后就可以成功运行了. 然后dmesg看下打印的信息:
最后
目前来看, 内核驱动模块好像比用户态程序难不了多少, 但是当程序复杂下去, 调试就会越发困难了, 不比用户态. 很多时候, 一个错误会很致命, 很多时候, 一个错误错得完全看不懂. 喜欢记得点赞, 有意见或者建议评论区见哦.
网友评论