> 2023/7/20 初學內核,記錄與分享,感嘆內核學了後真的感覺很多東西都通透了,但是難度太大,只能淺淺初探。 # 前提 內核五大功能 ➢ **進程管理**:進程的創建,銷毀,調度等功能 註:可中斷,不可中斷,就是是否被信號打斷。從運行狀態怎樣改到可中斷等待態,和不可中斷等待態操作系統開始會對每 ...
2023/7/20 初學內核,記錄與分享,感嘆內核學了後真的感覺很多東西都通透了,但是難度太大,只能淺淺初探。
前提
內核五大功能
➢ 進程管理:進程的創建,銷毀,調度等功能
註:可中斷,不可中斷,就是是否被信號打斷。從運行狀態怎樣改到可中斷等待態,和不可中斷等待態操作系統開始會對每個進程分配一個時間片,當進程裡面寫了sleep函數,進程由運行到休眠態,但是此時CPU不可能等著。有兩種方法,1:根據時間片,CPU自動跳轉,2:程式裡面自己寫能引起CPU調度的代碼就可以
➢ 文件管理:通過文件系統ext2/ext3/ext4 yaff jiffs等來組織管理文件
➢ 網路管理:通過網路協議棧(OSI,TCP)對數據進程封裝和拆解過程(數據發送和接收是通過網卡驅動完成的,網卡驅動不會產生文件(在Linux系統dev下麵沒有相應的文件),所以不能用open等函數,而是使用的socket)。
➢ 記憶體管理:通過記憶體管理器對用戶空間和內核空間記憶體的申請和釋放
➢ 設備管理: 設備驅動的管理(驅動工程師所對應的)
✧ 字元設備驅動: (led 滑鼠 鍵盤 lcd touchscreen(觸摸屏))
1.按照位元組為單位進行訪問,順序訪問(有先後順序去訪問)
2.會創建設備文件,open read write close來訪問
✧ **塊設備驅動 ** :(camera u盤 emmc)
1.按照塊(512位元組)(扇區)來訪問,可以順序訪問,可以無序訪問
2.會創建設備文件,open read write close來訪問
✧ 網卡設備驅動:(貓)
1.按照網路數據包來收發的。
驅動
三要素:入口,出口,許可證
● 入口:資源的申請
● 出口:資源的釋放
● 許可證:GPL(寫一個模塊需要開源,因為Linux系統是開源的,所以需要寫許可協議)
1.基礎模塊
驅動格式
#include <linux/init.h>
#include<linux/module.h>
//__init將hello_init放到.init.text段中
static int __init hello_init(void)
{
return 0;
}
//__exit將hello_exit放到.exit.text段中
static void __exit hello_exit(void)
{
}
//告訴內核驅動的入口地址(函數名為函數首地址)
module_init(hello_init);
//告訴內核驅動的出口地址
module_exit(hello_exit);
//許可證
MODULE_LICENSE("GPL");
makefile格式
//板子內核路徑
#KERNEL_PATH=/home/hq/kernel/kernel-3.4.39
//Ubuntu內核的路徑
KERNEL_PATH=/lib/modules/$(shell uname -r)/build
PWD=$(shell pwd) //驅動文件的路徑
all: //目標
make -C $(KERNEL_PATH) M=$(PWD) modules //(-C:進入頂層目錄)
/*註:進入內核目錄下執行make modules這條命令
如果不指定 M=$(PWD) 會把內核目錄下的.c文件編譯生成.ko*/
.PHONY:clean
clean:
make -C $(KERNEL_PATH) M=$(PWD) clean
obj-m = hello.o //指定編譯模塊的名字
命令的使用
創建索引文件
ctags -R
在終端上
vi -t xxx
在代碼中跳轉
ctrl + ]
ctrl + t
sudo insmod hello.ko 安裝驅動模塊
sudo rmmod hello 卸載驅動模塊
lsmod 查看模塊
dmesg 查看消息
sudo dmesg -C 直接清空消息不回顯
sudo dmesg -c 回顯後清空
列印函數
概念
1. #include <linux/printk.h> //增加這個頭文件
2. printk(KERN_ERR "Fail%d",a);//列印函數,KERN_ERR對應的是內核列印級別
3. grep "printk" * -nR 檢索所有的列印函數
vi -t KERN_ERR(查看內核列印級別)
4.
#define KERN_EMERG "<0>" /* system is unusable */(系統不用)
#define KERN_ALERT "<1>" /* action must be taken immediately */(被立即處理)
#define KERN_CRIT "<2>" /* critical conditions */(臨界條件,臨界資源)
#define KERN_ERR "<3>" /* error conditions */(出錯)
#define KERN_WARNING "<4>" /* warning conditions */(警告)
#define KERN_NOTICE "<5>" /* normal but significant condition */(提示)
#define KERN_INFO "<6>" /* informational */(列印信息時候的級別)
#define KERN_DEBUG "<7>" /* debug-level messages */ (調試級別)
0 ------ 7
最高的 最低的
5. 列印顯示效果是有優先順序的
inux@ubuntu:~$ cat /proc/sys/kernel/printk 查看優先順序,分佈為以下的四個級別
終端的級別 消息的預設級別 終端的最大級別 終端的最小級別
4 4 1 7
更改方式為 echo 4 3 1 7 > /pro/sys/kernel/printk(切換成su許可權)
如果是更改開發板列印級別 vi rootfs/etc/init.d/rcS
echo 4 3 1 7 > /proc/sys/kernel/printk
驅動多文件編譯
概念
hello.c add.c
Makefile
obj-m:=demo.o
demo-y+=hello.o add.o
(-y作用:將hello.o add.o放到demo.o中)
最終生成demo.ko文件
舉例
#KERNEL_PATH=/home/hq/kernel/kernel-3.4.39
KERNEL_PATH=/lib/modules/$(shell uname -r)/build
PWD=$(shell pwd)
all:
make -C $(KERNEL_PATH) M=$(PWD) modules
.PHONY:clean
clean:
make -C $(KERNEL_PATH) M=$(PWD) clean
obj-m = printkk.o
demo-y+=add.o
demo-y+=sub.o
demo-y+=printkk.o
模塊傳遞函數
概念
module_param(name, type, perm)
功能:接收命令行傳遞的參數
參數:
@name :變數的名字
@type :變數的類型
@perm :許可權 0664 0775(其它用戶對我的只有讀和執行許可權,沒有寫的許可權)
modinfo hello.ko(查看變數情況)
MODULE_PARM_DESC(_parm, desc)
功能:對變數的功能進行描述
參數:
@_parm:變數
@desc :描述欄位
只能傳十進位,不可以寫十六進位
module_param_array(name, type, nump, perm)
功能:接收命令行傳遞的數組
參數:
@name :數組名
@type :數組的類型
@nump :參數的個數,變數的地址
@perm :許可權
舉例
#include <linux/init.h>
#include<linux/module.h>
#include<linux/printk.h>
#include"head.h"
//入口申請資源
int a=10;
module_param(a,int,0664);
MODULE_PARM_DESC(a,"this is lcd light");
short b=11;
module_param(b,short,0774);
MODULE_PARM_DESC(b,"this is rgb");
char ch='c';
module_param(ch,byte,0664);
MODULE_PARM_DESC(ch,"this is ch val");
char *p=NULL;
module_param(p,charp,0664);
MODULE_PARM_DESC(p,"this is ch *p");
int ww[10]={0};
int num;
module_param_array(ww,int,&num,0664);
MODULE_PARM_DESC(p,"this is int array [10]");
static int __init printk_init(void)
{
int i;
printk("a is val=%d\n",a);
printk("b is val=%d\n",b);
printk("c is val=%c\n",ch);
printk("p is val=%s\n",p);
for (i = 0; i < num; i++)
{
printk("ww[%d]==%d\n",i,ww[i]);
}
return 0;
}
//出口函數釋放資源
static void __exit printk_exit(void)
{
}
//入口
module_init(printk_init);
//出口
module_exit(printk_exit);
MODULE_LICENSE("GPL");
2.字元設備驅動
概念理解
硬體與軟體的連接,需要驅動。
驅動是寫在內核層的。
應用層是調用內核層提供的介面實現的。
所以,這三者之間一定是有個東西作為聯繫的。那就是設備號。
以LED為例 字元設備的 步驟:
1.註冊字元設備驅動 - 得到一個字元設備驅動的框架,並且得到設備號
2.確定操作的硬體設備 - led燈(初始化燈)
3.初始化燈(先建立燈實際物理地址和虛擬地址之間的映射)- 基於操作系統開發,操作虛擬記憶體,
4.用戶空間數據拷貝到內核空間數據的交互(用戶使用的時候,驅動才會被真正運行,涉及數據交互)
5.在應用層創建一個設備文件(設備節點)
函數框架
int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
功能:註冊一個字元設備驅動
參數:@major:主設備號
:如果你填寫的值大於0,它認為這個就是主設備號
:如果你填寫的值為0,操作系統給你分配一個主設備號
@name :名字 cat /proc/devices
@fops :操作方法結構體
返回值:major>0 ,成功返回0,失敗返回錯誤碼(負數) vi -t EIO
major=0,成功主設備號,失敗返回錯誤碼(負數)
cat /proc/devices 查看系統自動分配的主設備號
void unregister_chrdev(unsigned int major, const char *name)
功能:註銷一個字元設備驅動
參數:
@major:主設備號
@name:名字
返回值:無
sudo mknod led (路徑是任意) c/b 主設備號 次設備號 //手動創建設備文件
/-------數據傳遞,角度是占在用戶角度來說
int copy_from_user(void *to, const void __user *from, int n)
功能:從用戶空間拷貝數據到內核空間(用戶需要寫數據的時候)
參數:
@to :內核中記憶體的首地址
@from:用戶空間的首地址
@n :拷貝數據的長度(位元組)
返回值:成功返回0,失敗返回未拷貝的位元組的個數
int copy_to_user(void __user *to, const void *from, int n)
功能:從內核空間拷貝數據到用戶空間(用戶開始讀數據)
參數:
@to :用戶空間記憶體的首地址
@from:內核空間的首地址
@n :拷貝數據的長度(位元組)
返回值:成功返回0,失敗返回未拷貝的位元組的個數
----------/
/-----物理地址轉為虛擬地址
void * ioremap(phys_addr_t offset, unsigned long size)
功能:將物理地址映射成虛擬地址
參數:
@offset :要映射的物理的首地址
@size :大小(位元組)(映射是以業為單位,一頁為4K,就是當你小於4k的時候映射的區域都為4k)
返回值:成功返回虛擬地址,失敗返回NULL((void *)0);
void iounmap(void *addr)
功能:取消映射
參數:
@addr :虛擬地址
返回值:無
----------------/
/-----設備節點自動創建
如果沒有寫節點自動創建,那麼就需要加
sudo mknod <名字> c <設備號> <子設備號>
設備號 就需要 cat /proc/devices 中查看到設備號
例如: sudo mknod led c 230 0
這樣在本地創建了字元設備命名為led的字元設備文件。
struct class *cls;
cls = class_create(owner, name) /void class_destroy(struct class *cls)//銷毀
功能:向用戶空間提交目錄信息(內核目錄的創建)
參數:
@owner :THIS_MODULE(看到owner就添THIS_MODULE)
@name :目錄名字
返回值:成功返回struct class *指針
失敗返回錯誤碼指針 int (-5)
struct device *device_create(struct class *class, struct device *parent,dev_t devt, void *drvdata, const char *fmt, ...)(內核文件的創建),每個文件對應一個外設(硬體設備)
/void device_destroy(struct class *class, dev_t devt)//銷毀
功能:向用戶空間提交文件信息
參數:
@class :目錄返回指針
@parent:NULL
@devt :設備號 (major<<12 |0 < = > MKDEV(major,0))
@drvdata :NULL
@fmt :文件的名字
返回值:成功返回struct device *指針
失敗返回錯誤碼指針 int (-5)
-------/
實例
實現控制LED閃爍
chrdev.c 文件
#include <linux/init.h>
#include <linux/module.h>
#include <linux/printk.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <asm/io.h>
#include <linux/device.h>
#define NAME "chrdev_led" //這個名字最終效果就是設備號旁的名字
//定義巨集表示實際物理首地址 這個地址是根據晶元手冊找到對應的首地址
#define RED_BASE 0xc001a000 //#GPIOA28
#define GREE_BASE 0xc001e000 // #GPIOE13
#define BLUE_BASE 0xc001b000 //#GPIOB12
unsigned int major = 0; //定義是為了接受創建的設備號
char kbuf[32] = {0};//實現拷貝,與應用層用戶進行數據交換的數組
//定義指針保存映射後的虛擬地址的首地址
unsigned int *red_addr = NULL; //這樣寫的好處在於+1就是移動四個位元組,對應單片機一個寄存器的大小
unsigned int *gree_addr = NULL;
unsigned int *blue_addr = NULL;
struct class *cls = NULL;//是為了創建設備節點做的準備
struct device *dev = NULL;
int chrdev_open(struct inode *node_t, struct file *file_t)//當用戶使用fopen就會調用這句
{
printk("%s %s %d\n", __FILE__, __func__, __LINE__);//測試用的
return 0;
}
ssize_t chrdev_read(struct file *file_t, char __user *ubuf, size_t n, loff_t *off_t)//當用戶使用讀操作會調用這句
{
printk("%s %s %d\n", __FILE__, __func__, __LINE__);
//將內核空間的數據拷貝到用戶空間
if (sizeof(kbuf) < n)
n = sizeof(kbuf); //ubuf是用戶,應用層的數據
if (copy_to_user(ubuf, kbuf, n) != 0)//當用戶使用讀操作會進入這個函數,然後再將內核數據給用戶
{
printk("copy_to_user err.");
return -EINVAL;
}
return 0;
}
ssize_t chrdev_write(struct file *file_t, const char __user *ubuf, size_t n, loff_t *off_t)
{
//當用戶執行寫操作驅動會進行這句操作
printk("%s %s %d\n", __FILE__, __func__, __LINE__);
if (sizeof(kbuf) < n)
n = sizeof(kbuf);
//將用戶空間的數據拷貝到內核空間
if (copy_from_user(kbuf, ubuf, n) != 0)
{
printk("copy_from_user err.");
return -EINVAL;
}
printk("kbuf=%s\n", kbuf);
if (kbuf[0] == 1)//根據用戶寫的內容做出相應的操作
{
//紅燈亮
*red_addr |= (1 << 28);//這就是控制對應寄存器的IO控制高低電平的
}
else if (kbuf[0] == 0)
{
//紅燈滅
*red_addr &= (~(1 << 28));
}
if (kbuf[1] == 1)
{
//紅燈亮
*gree_addr |= (1 << 13);
}
else if (kbuf[1] == 0)
{
//紅燈滅
*gree_addr &= (~(1 << 13));
}
if (kbuf[2] == 1)
{
//紅燈亮
*blue_addr |= (1 << 12);
}
else if (kbuf[2] == 0)
{
//紅燈滅
*blue_addr &= (~(1 << 12));
}
return 0;
}
int chrdev_close(struct inode *node_t, struct file *file_t)//用戶執行close操作進入這句
{
printk("%s %s %d\n", __FILE__, __func__, __LINE__);
return 0;
}
struct file_operations fops = { //用戶之所以執行對應的操作就能跳轉對應的位置就是因為這個結構體的原因
//這個結構體的名字就對應了驅動三模塊的申請中需要的參數
.open = chrdev_open,
.read = chrdev_read,
.write = chrdev_write,
.release = chrdev_close,
};
//入口函數-申請資源
static int __init printk_init(void)
{
//註冊字元設備驅動
major = register_chrdev(major, NAME, &fops);
if (major < 0)
{
printk("register_chrdev err.");
return -EINVAL;
}
//初始化燈-引腳功能(GPIO) 輸出功能 滅
//1.基於操作系統開發。建立燈物理地址和虛擬地址之間映射
//紅燈
red_addr = (unsigned int *)ioremap(RED_BASE, 40);//基於地址開闢一定的範圍映射,實際上為4k大小
if (red_addr == NULL)
{
printk("ioremap err.");
return -EINVAL;
}
//gree
gree_addr = (unsigned int *)ioremap(GREE_BASE, 40);
if (gree_addr == NULL)
{
printk("ioremap gree err.");
return -EINVAL;
}
blue_addr = (unsigned int *)ioremap(BLUE_BASE, 40);
if (blue_addr == NULL)
{
printk("ioremap blue err.");
return -EINVAL;
}
//通過虛擬地址操作實際物理地址向對應寄存器寫值
//配置引腳GPIO
//配置輸入輸出模式、復用選用、高點電平
*(red_addr + 9) &= (~(3 << 24));
*(red_addr + 1) |= (1 << 28); //輸出模式
*red_addr &= (~(1 << 28));
*(gree_addr + 8) &= (~(3 << 26));
*(gree_addr + 1) |= (1 << 13);
*gree_addr &= (~(1 << 13));
*(blue_addr + 8) |= (1 << 25);
*(blue_addr + 8) &= (~(1 << 24));
*(blue_addr + 1) |= (1 << 12);
*blue_addr &= (~(1 << 12));
//設置自動創建設備節點
//1.提交目錄信息
cls = class_create(THIS_MODULE, NAME);//這個NAME為上面的巨集定義
if (IS_ERR(cls))//判斷是否再這範圍內的函數
{
printk("class_create err.");
return -EINVAL;
}
//2.提交文件信息
dev = device_create(cls, NULL, MKDEV(major, 0), NULL, "led");
if (IS_ERR(dev))
{
printk("class_create err.");
return -EINVAL;
}
return 0;
}
//出口函數-釋放資源(先申請的後釋放,後申請先釋放)
static void __exit printk_exit(void)
{
//銷毀創建的設備節點
device_destroy(cls,MKDEV(major,0));
class_destroy(cls);
//取消映射
iounmap(blue_addr);
iounmap(gree_addr);
iounmap(red_addr);
//註銷設備驅動
unregister_chrdev(major, NAME);
}
module_init(printk_init);
module_exit(printk_exit);
MODULE_LICENSE("GPL");
app.c文件
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, const char *argv[])
{
int fd = open("/dev/led", O_RDWR);//這個是會生成在該文件
char buf[32] = {1, 0, 0}; //buf[0]-red buf[1]-gree buf[2]-blue
while (1) //紅燈亮一秒滅一秒
{
buf[0] = 0;
buf[1] = 0;
buf[2] = 0;
write(fd, buf, sizeof(buf));
sleep(1);
buf[0] = 0;
buf[1] = 0;
buf[2] = 1;
write(fd, buf, sizeof(buf));
sleep(1);
buf[0] = 0;
buf[1] = 1;
buf[2] = 0;
write(fd, buf, sizeof(buf));
sleep(1);
buf[0] = 0;
buf[1] = 1;
buf[2] = 1;
write(fd, buf, sizeof(buf));
sleep(1);
buf[0] = 1;
buf[1] = 0;
buf[2] = 0;
write(fd, buf, sizeof(buf));
sleep(1);
buf[0] = 1;
buf[1] = 0;
buf[2] = 1;
write(fd, buf, sizeof(buf));
sleep(1);
buf[0] = 1;
buf[1] = 1;
buf[2] = 0;
write(fd, buf, sizeof(buf));
sleep(1);
buf[0] = 1;
buf[1] = 1;
buf[2] = 1;
write(fd, buf, sizeof(buf));
sleep(1);
}
close(fd);
return 0;
}
Makefile文件
KERNEL_PATH=/home/hq/kernel/kernel-3.4.39 #開發板內核路徑
#KERNEL_PATH=/lib/modules/$(shell uname -r)/build
#pc電腦上的路徑 gcc
PWD=$(shell pwd)
all:
make -C $(KERNEL_PATH) M=$(PWD) modules
#在內核頂層目錄執行make modules才可以將hello.c生成驅動
#-C 路徑:找到這個路徑執行這個命令
.PHONY:clean
clean:
make -C $(KERNEL_PATH) M=$(PWD) clean
obj-m = chrdev.o
表現效果
a.out文件因為是在開發板運行,所以是交叉編譯工具編譯
make後會生成chrdev.ko驅動文件,進行安裝後
輸入cat proc/devices可以看到 發現,剛剛我強調的名字就是出現在這裡
對應的設備號,因為是是自動創建設備節點了,所以在輸入 cd dev
就看到了led了,現在在執行./a.out 就可以看到效果了