文章目录
linux与windows的操作系统内核都是宏内核
内核编程特点:
- 不能使用C库函数,如外部头文件,
printf
,可以使用如printk
函数 - 可以使用汇编语言,GNU C和GCC编译器
- 不能使用FPU
- 可能会产生内存故障
编写的内核模块必须有下面两种函数
-
static int hello_int(void){...} // static int __init hello_init(void) module_init(hello_init);
通过
module_init(...);
来指定程序开始运行的函数 -
static void hello_exit(void){...} // static void __exit hello_exit(void) module_exit(hello_exit);
通过
module_exit(...);
来指定程序终结时运行的函数,一般在这个exit函数,会释放申请的各种系统资源,最后退出,整个模块从内核中卸载
printk
的使用
…
内核模块的两种形态
-
静态编译进内核的模块
-
用
insmod
命令动态加载的模块用
rmmod
来卸载模块sudo insmod hello.ko
var=XXXsudo rmmod hello.ko
内核的两种声明
MODULE_LICENSE("GPL");
内核是GNU开源工程的一部分,遵循GPL协议,使用MODULE_LICENSE可以声明你编写的这段代码使用设什么协议发布,一般声明GPL就可以了,不声明的话,内核可能不会接受,运行时的打印可能打不出来
MODULE_AUTHOR("wit@zhaixue.cc");
MODULE_AUTHOR声明代码作者,一般可以留个邮箱或者名字,对这块代码感兴趣的人可以通过该联系方式找到你
需要包含的头文件
#include <linux/init.h>
#include <linux/module.h>
因为我们在内核模块中使用了module_init
、module_exit
函数,按照C语言先声明后使用的传统,所以要包含module.h
头文件,这个头文件位于include/linux
目录下,include是路径起点,所以要给头文件价格前缀:linux/module.h
。init.h
头文件主要定义了__init
、__exit
宏,用于将这个内核模块在编译时放到指定的section中,统一管理。
一个内核模块的简单示例
#include<linux/init.h>
#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/moduleparam.h>
#include<linux/slab.h>
MODULE_LICENSE("GPL");
static char *whom = "world";
static int num = 10;
//传递命令参数 S_IRUGO 指明参数可以被所有人读取
module_param(whom, charp, S_IRUGO
);
module_param(num,
int,S_IRUGO);
typedef struct Node //typedef方法函数可以对于struct Node进行重命名
{
int val;
struct Node *next;
} listnode, *LinkList; //定义一个结构体指针方便后续的操作
listnode *newNode(int val) {
listnode *L = (listnode *) kmalloc(sizeof(listnode), GFP_KERNEL); //为头结点动态分配内存
L->next = NULL; //初始化时头结点指向空
L->val = val;
return L;
}
int insertNode(int val, listnode *head) {
listnode *n = newNode(val);
listnode *tail = head;
while (tail->next) {
tail = tail->next;
}
tail->next = n;
head->val += 1;
return 1;
}
int deleteNode(int val, listnode *head) {
listnode *b = head->next;
listnode *a = head;
int i = 0;
for (; i < head->val; i++) {
if (b->val == val) {
a->next = b->next;
kfree(b);
head->val -= 1;
return 1;
} else {
a = b;
b = b->next;
}
}
return -1;
}
int lookupNode(int val, listnode *head) {
listnode *b = head->next;
int i = 0;
for (; i < head->val; i++) {
if (b->val == val) {
return 1;
} else {
b = b->next;
}
}
return -1;
}
void printListNode(listnode *head) {
listnode *b = head->next;
int i = 0;
for (; i < head->val - 1; i++) {
printk(KERN_ALERT
"%d,", b->val);
b = b->next;
}
printk(KERN_ALERT
"%d,", b->val);
}
//程序中必须有下列两个函数
static int hello_init(void) {
LinkList head = newNode(0);
int i;
int result;
for (i = 0; i < num; i++) {
insertNode(i, head);
}
result = lookupNode(5, head);
printk(KERN_ALERT
"find 5 is %d", result);
deleteNode(5, head);
result = lookupNode(5, head);
printk(KERN_ALERT
"find 5 is %d", result);
printListNode(head);
while (head) {
listnode *n = head;
head = head->next;
kfree(n);
}
return 0;
}
static void hello_exit(void) {
printk(KERN_ALERT
"goodbye,kernel/n");
}
//加载or卸载模块
module_init(hello_init);
module_exit(hello_exit);
// 可选
MODULE_AUTHOR("zhou-silence");
MODULE_DESCRIPTION("This is a simple example!/n");
MODULE_VERSION("v1.0");
MODULE_ALIAS("A simplest example");
obj-m:=hello.o
CURRENT_PATH :=$(shell pwd)
VERSION_NUM :=$(shell uname -r)
LINUX_PATH :=/usr/src/linux-headers-$(VERSION_NUM)
all :
make -C $(LINUX_PATH) M=$(CURRENT_PATH) modules
clean :
make -C $(LINUX_PATH) M=$(CURRENT_PATH) clean
root@duyuhui:~/experiment# ls
hello.c Makefile
root@duyuhui:~/experiment# make
make -C /usr/src/linux-headers-5.15.0-71-generic M=/root/experiment modules
make[1]: Entering directory '/usr/src/linux-headers-5.15.0-71-generic'
warning: the compiler differs from the one used to build the kernel
The kernel was built by: gcc (Ubuntu 11.3.0-1ubuntu1~22.04.1) 11.3.0
You are using: gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
CC [M] /root/experiment/hello.o
MODPOST /root/experiment/Module.symvers
CC [M] /root/experiment/hello.mod.o
LD [M] /root/experiment/hello.ko
BTF [M] /root/experiment/hello.ko
Skipping BTF generation for /root/experiment/hello.ko due to unavailability of vmlinux
make[1]: Leaving directory '/usr/src/linux-headers-5.15.0-71-generic'
root@duyuhui:~/experiment#
root@duyuhui:~/experiment# insmod hello.ko
root@duyuhui:~/experiment# rmmod hello.ko
root@duyuhui:~/experiment# dmesg
......
[ 502.824660] find 5 is 1
[ 502.824758] find 5 is -1
[ 502.824817] 0,
[ 502.824869] 1,
[ 502.824918] 2,
[ 502.824967] 3,
[ 502.825019] 4,
[ 502.825071] 6,
[ 502.825121] 7,
[ 502.825171] 8,
[ 502.825221] 9,
[ 506.827271] goodbye,kernel/n
Makefile文件简介
Makefile文件干啥的
在windows下的各种IDE可以帮助自动运行代码,但是到了linux环境下,需要使用Makefile文件来自动编译代码,一旦整个项目的Makefile都写好了以后,只需要一个简单的make命令,就可以实现自动编译了。当然,准确的说,是make这个命令工具帮助我们实现了我们想要做的事,而Makefile就相当于是一个规则文件,make程序会按照Makefile所指定的规则,去判断哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译
obj-m
与obj-y
obj-m
表示把文件XXX.o
作为模块进行编译,不会编译到内核,但是会生成一个独立的XXX.ko
文件
obj-y
表示把XXX.o
文件编译进内核
ifeq
andifneq
andifdef
andifndef
-
ifeq
是判断是否相等,如果相等就执行。其格式有4种
ifeq (ARG1, ARG2) 如果相等就执行 ifeq 'ARG1' 'ARG2' 如果相等就执行 ifeq "ARG1" "ARG2" 如果相等就执行 ifeq "ARG1" 'ARG2' 如果相等就执行
ifeq ($(strip $(foo)),) 如果为空就执行 endif
如果ARG1与ARG2相等就执行
-
ifneq
与ifeq
相反 -
ifdef
判断一个变量是否已经定义了格式为
ifdef VARIABLE-NAME
ifdef
只是测试一个变量是否有值,不会对变量进行替换展开来判断变量的值是否为空。除开
VARIABLE-NAME=
这种情况以外,使用其它方式对它的定义都会使ifdef
返回真。就是说,即使我们通过其它方式(比如,定义它的值引用了其它的变量)给它赋了一个空值,ifdef
也会返回真。例子:
例1: bar = foo = $(bar) ifdef foo frobozz = yes else frobozz = no endif 例 2: foo = ifdef foo frobozz = yes else frobozz = no endif
例1的结果是:
frobozz = yes
例2的结果是:
frobozz = no
-
ifndef与ifdef相反
KERNELRELEASE
ifeq ($(KERNELRELEASE),)
它的由来是指在Linux源码根目录下的Makefile编译内核时,KERNELRELEASE宏会被定义,这个时候非空
make -C $(KDIR) M=$(PWD)
其中-C
指明到$(KDIR)
目录下读取Makefile,M=$(PWD)
表明返回到当前目录继续读入,执行当前的Makefile。
Makefile中的$@
、$^
、$<
、$?
、
%、`
+、
$*`
$@ 表示目标文件
$^ 表示所有的依赖文件
$< 表示第一个依赖文件
$? 表示比目标还要新的依赖文件列表
C语言编写问题
[Error] ‘for‘ loop initial declarations are only allowed in C99 or C11 mode 解决方法
这是因为在 GCC 中直接在 for 循环中初始化了增量, 这种写法在 GCC 中是错误的,必须先定义变量i,然后可以成功编译运行。
initializer element is not constant 问题
C语言初始化一个全局变量或static变量时,只能用常量赋值,不能用变量赋值!
(30条消息) initializer element is not constant 问题_沈万三gz的博客-CSDN博客
kmalloc函数使用
kmalloc函数详解 - 森码世界 - 博客园 (cnblogs.com)
在linux内核中不能使用malloc
kfree函数的使用
直接kfree(对象)
即可,值得注意的是你需要手动回收资源,否则系统不会帮你回收
一些其他问题
printk打印不出来的解决方法
linux内核打印数据到串口控制台,printk数据不打印问题_广州建站小戴BOTAO博客 (yii666.com)
这里死活打印不出来,就直接使用dmesg了
insmod第一次killed,第二次直接卡死的原因
(32条消息) 关于模块insmod和rmmod出错的解决方案_rmmod卡死_Victor–的博客-CSDN博客
系统调用
-
系统调用都具有一种明确的操作
如:
getpid()
系统调用,根据定义它会返回当前进程的PID实现如下:
asmlinkage long sys_getpid(void){ return current->tgid; }
asmlinkage限定词,通知编译器仅从栈中提取该函数的参数,系统调用都需要这个限定词,系统调用
get_pid()
在内核中被定义为sys_getpid()
-
在Linux系统中,每个系统调用被赋予一个系统调用号,用户空间中的进程执行一个系统调用的时候,这个系统调用号就被用来指明哪个系统调用,系统调用号存储在
sys_call_table
中,它与体系结构有关,一般在entry.s
中定义 -
用户如何使用系统调用
-
必须保证系统调用是可重入的
内核下载
用 apt-cache search linux-source
和 apt-get install linux-source
可以搜索并下载当前内核版本的源码,并自动解压到 /usr/src/
目录下。
使用tar xvf linux-source-4.4.0.tar.gz
来解压。
linux 5.15.0
等一系列内核的系统调用需要修改的目录为
kernel/sys.c
是用于编写具体函数的,如下所示setuid就是一个声明后在这里编写具体函数
include/linux/syscalls.h
是用于声明函数的,即asmlinage开头的,如下所示,setuid即为一个系统调用的声明
arch/x86/entry/syscalls/syscall_64.tbl
是系统调用号表
编写系统调用
下面的操作都是在下载好的内核里面操作,然后编译内核,安装内核,最终启动内核,然后写个程序测试是否可以使用
- 修改系统调用表号,如下,设置了调用表449号为我们的系统调用
-
增加系统调用声明,如下,增加了系统调用声明asmlinkage开头的
-
增加系统调用实际内容,如下系统调用的内容是打印一个Hello eternal robot
-
编译内核
Linux内核添加新的系统调用 教程 - 付杰博客 (fujieace.com)
Linux重新编译内核_51CTO博客_ubuntu编译linux内核(这篇很详细)
简单来说就是在下载内核解压根目录下运行下面内容
make clean make defconfig make make modules_install make install
在虚拟机里面,输入
reboot
加回车后,长按esc
键,才能进入选内核界面 -
测试内核
#include <unistd.h> #include <sys/syscall.h> #include <sys/types.h> #include <stdio.h> #define __NR_hello_eternal_robot 448 int main(int argc, char *argv[]) { syscall(__NR_hello_eternal_robot); printf("ok! run dmesg | grep Hello in terminal!\n"); return 0; }
使用如下编译运行
root@duyuhui:~/experiment/2# nano hello.c root@duyuhui:~/experiment/2# gcc -o myhello hello.c root@duyuhui:~/experiment/2# ./myhello ok! run dmesg | grep Hello in terminal! root@duyuhui:~/experiment/2# dmesg | grep Hello [ 707.558821] Hello eternal robot [ 1413.170153] Hello eternal robot [ 1462.344935] Hello eternal robot root@duyuhui:~/experiment/2#
主要运行了三次,所以抓出了三个Hello内容
遇到的一些问题
-
什么是SYSCALL_DEFINE
-
安装内核的时候遇到
E:You must put some 'deb-src' URIs in your sources.list
怎么办? -
内核编译的时候在输入
make -j 5
后出现gelf.h: No such file or directory
问题(34条消息) Ubuntu编译出现:gelf.h: No such file or directory_木可木可❀的博客-CSDN博客
-
自定义内核的时候在输入
make defconfig
后出现lexer.lex.c Error 127
问题(34条消息) make menuconfig出现错误lexer.lex.c Error 127_scripts/kconfig/lexer.lex.c_qt码农C的博客-CSDN博客
linux内核模块调用其他模块导出的函数
遇到的一些问题
-
ERROR: modpost: “Func” [xxxxx.ko]undefined!
问题,下面给出了三种方式解决,这个里面的第二点解决方案有点问题,会导致下面的第二点错误no symbol version for fptr_Operation
-
no symbol version for fptr_Operation
关于内核模块挂载出现“no symbol version for”问题的研究-tekkamanninja-ChinaUnix博客
-
如何查看导出的函数,或查看函数是否导出了
cat /proc/kallsyms | grep hello
-
如何计算内核模块时间
linux字符设备驱动程序编写方式
Linux的设备管理是和文件系统紧密结合的,各种设备都以文件的形式存放在/dev目录 下,称为设备文件。应用程序可以打开、关闭和读写这些设备文件,完成对设备的操作,就像操作普通的数据文件一样。
为了管理这些设备,系统为设备编了号,每 个设备号又分为主设备号和次设备号。主设备号用来区分不同种类的设备,而次设备号用来区分同一类型的多个设备。对于常用设备,Linux有约定俗成的编 号,如硬盘的主设备号是3。
Linux为所有的设备文件都提供了统一的操作函数接口,方法是使用数据结构struct file_operations。这个数据结构中包括许多操作函数的指针,如open()、close()、read()和write()等,但由于外设 的种类较多,操作方式各不相同。Struct file_operations结构体中的成员为一系列的接口函数,如用于读/写的read/write函数和用于控制的ioctl等。
打开一个文件就是调用这个文件file_operations中的open操作。不同类型的文件有不同的file_operations成员函数,如普通的磁盘数据文件, 接口函数完成磁盘数据块读写操作;而对于各种设备文件,则最终调用各自驱动程序中的I/O函数进行具体设备的操作。这样,应用程序根本不必考虑操作的是设 备还是普通文件,可一律当作文件处理,具有非常清晰统一的I/O接口。所以file_operations是文件层次的I/O接口。
mknod
命令用于创建字符设备文件和块设备文件
mknod (选项) (参数)
选项有:
-Z:设置安全的上下文; -m:设置权限模式; -help:显示帮助信息; --version:显示版本信息。
参数有:
- 文件名:要创建的设备文件名;
- 类型:指定要创建的设备文件的类型;
- 主设备号:指定设备文件的主设备号;
- 次设备号:指定设备文件的次设备号。
例如:
mknod /dev/ttyUSB32 c 188 32
/dev/ttyUSB32,表示要创建的节点文件
c,表示是一个字符设备
188,主设备号,表示某一个具体的驱动
32,次设备号,表示使用这个驱动的各个设备
Linux中/proc目录下文件详解
/proc文件系统下的多种文件提供的系统信息不是针对某个特定进程的,而是能够在整个系统范围的上下文中使用。可以使用的文件随系统配置的变化而变化。
/proc文件系统是一个伪文件系统,它只存在内存当中,而不占用外存空间。
它以文件系统的方式为访问系统内核数据的操作提供接口。
由于系统的信息,如进程,是动态改变的,所以用户或应用程序读取proc文件时,proc文件系统是动态从系统内核读出所需信息并提交的。
下面列举一些类型
/proc/cmdline
内核启动的命令行
/proc/cpuinfo
/proc/devices
列出字符和块设备的主设备号,以及分配到这些设备号的设备名称
/proc/filesystems
列出可供使用的文件系统类型,一种类型一行。虽然它们通常是编入内核的文件系统类型,但该文件还可以包含可加载的内核模块加入的其它文件系统类型
/proc/meminfo
遇到的一些问题
struct file_operations
的头文件是#include <linux/fs.h>
copy_to_user
的头文件是#include <linux/uaccess.h>
步骤
-
注册字符设备
-
编写字符设备模块,以及载入内核,查看一下是否成果
编写my_char_device.c代码
#include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/types.h> #include <linux/uaccess.h> #include <linux/fs.h> // 主设备号 #define CHAR_DEVICE_MAJOR 255 // 设备名 #define CHAR_DEVICE_NAME "my_char_devices" // 保存数据的最大长度 #define MSG_LENGTH 1024 // 设备open次数 static int device_open_counts=0; // 保存从用户空间来的数据 static char message[MSG_LENGTH]={0}; // 记录用户空间的数据的长度 static short message_length=0; MODULE_LICENSE("GPL"); MODULE_AUTHOR("dyh"); MODULE_DESCRIPTION("自建字符设备/n"); static int device_open(struct inode *inode, struct file *f) { device_open_counts++; printk("%s 已经打开了 %d 次\n",CHAR_DEVICE_NAME,device_open_counts); return 0; } static int device_release(struct inode *inode, struct file *f) { printk("%s 关闭\n",CHAR_DEVICE_NAME); return 0; } static ssize_t device_read(struct file *f, char __user *buffer, size_t count, loff_t *pOffset) { int ret=0; ret=copy_to_user(buffer,message,message_length); if(ret==0) { printk("%s 发送 %d 数量的字符给用户\n",CHAR_DEVICE_NAME,message_length); return 0; } else { printk("%s 读取数据失败\n",CHAR_DEVICE_NAME); return -1; } } static ssize_t device_write(struct file *f, const char __user *buffer, size_t count, loff_t *pOffset) { int ret=0; ret=copy_from_user(message,buffer,count); message_length=count; if(ret==0) { printk("%s 接收来自用户的数据数量是 %d\n",CHAR_DEVICE_NAME,count); return 0; } else { printk("%s 接收数据失败\n",CHAR_DEVICE_NAME); return -1; } } // 设备操作函数结构体 static struct file_operations device_operations={ .owner=THIS_MODULE, .open=device_open, .release=device_release, .read=device_read, .write=device_write, }; static int __init char_device_driver_init(void) { printk("my_char_device 初始化\n"); int ret=0; // 注册字符设备驱动 ret=register_chrdev(CHAR_DEVICE_MAJOR,CHAR_DEVICE_NAME,&device_operations); if(ret<0) { printk("%s 设备注册失败\n",CHAR_DEVICE_NAME); } return 0; } static void __exit char_device_driver_exit(void) { // 注销字符设备驱动 unregister_chrdev(CHAR_DEVICE_MAJOR,CHAR_DEVICE_NAME); printk("my_char_device 退出\n"); } module_init(char_device_driver_init); module_exit(char_device_driver_exit);
编写makefile文件
obj-m:=my_char_device.o CURRENT_PATH :=$(shell pwd) VERSION_NUM :=$(shell uname -r) LINUX_PATH :=/usr/src/linux-headers-$(VERSION_NUM) all : make -C $(LINUX_PATH) M=$(CURRENT_PATH) modules clean : make -C $(LINUX_PATH) M=$(CURRENT_PATH) clean
make insmod my_char_device.ko lsmod grep | my_char_device ls /dev cat /proc/devices
第一行是编译,第二行是载入,第三行是查看载入是否成功,第四行是列出设备,第五行是列出设备
-
编写测试程序
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <string.h> #define LENGTH 1024 static char message[LENGTH]; int main() { int result = 0, fd; char to_send[LENGTH] = {0}; fd = open("/dev/my_char_device", O_RDWR); if (fd < 0) { printf("无法打开 my_char_device\n"); return 0; } printf("给内核模块输入一些东西:\n"); scanf("%[^\n]%*c", to_send); printf("正在写入...\n"); result = write(fd, to_send, strlen(to_send)); if (result < 0) { printf("写入失败\n"); return -1; } printf("按Enter键获取从内核发回的消息\n"); getchar(); printf("正在读取...\n"); result = read(fd, message, LENGTH); if (result < 0) { printf("从设备中读取失败\n"); return -1; } printf("信息是: [%s]\n", message); close(fd); return 0; }
root@duyuhui:~/experiment/4# gcc -o test test.c test.c: In function ‘main’: test.c:21:14: warning: implicit declaration of function ‘write’; did you mean ‘fwrite’? [-Wimplicit-function-declaration] 21 | result = write(fd, to_send, strlen(to_send)); | ^~~~~ | fwrite test.c:29:14: warning: implicit declaration of function ‘read’; did you mean ‘fread’? [-Wimplicit-function-declaration] 29 | result = read(fd, message, LENGTH); | ^~~~ | fread test.c:35:5: warning: implicit declaration of function ‘close’; did you mean ‘pclose’? [-Wimplicit-function-declaration] 35 | close(fd); | ^~~~~ | pclose root@duyuhui:~/experiment/4# ls device test test.c root@duyuhui:~/experiment/4# ./test 给内核模块输入一些东西: sx 正在写入... 按Enter键获取从内核发回的消息 正在读取... 信息是: [sx]