---
# System prepended metadata

title: 2021 LDD3 第3章
tags: [Linux Device Drivers, 'Linux ']

---

2021 LDD3 第3章
===

contribute by `Liao712148`
[Liao712148](https://github.com/Liao712148/2021ldd)
###### tags: `Linux Device Drivers`, `Linux `

寫驅動程式的第一步，是==定義你的驅動程式對user-space的程式提供那些功能==。


#### 名詞解釋
*    裝置編號  
    *    主編號:代表裝置(指scull從kernel配置而來的記憶體)所配合的驅動程式。
         可以透過由<linux/kdev_t.h>當中的macro `MAJOR(det_t dev);`來得到。
    *    次編號:代表你的驅動程式所實作的裝置，驅動程式可以直接從核心取得一個指向目標裝置的指標。
        可以透過由<linux/kdev_t.h>當中的macro `MINOR(det_t dev);`來得到。
    *   裝置編號可以利用 `MKDEV(int major, int minor)`來得到。

#### 向kernel索取裝置編號
`int alloc_chrdev_region(dev_t *dev, unsigned int firstmintor,   unsigned int count, char *name);`
透過`dev`取得配置範圍的第一個裝置編號。

#### /proc/devices 和 /dev的關係
`/proc/devices/`中的裝置是通過insmod載入到核心的，它可產生一個major供mknod作為 引數。 
`/dev/* `是通過mknod加上去的，格式:mknod device1 c/b major minor 如：mknod /dev/ttyS0 c 4 64，使用者通過此裝置名來訪問你的驅動。

#### 透過script 來載入scull module
透過scull_load來載入module
```shell=
#!/bin/sh
module="scull"
device="scull"
mode="664" 
# Group: since distributions do it differently, look for wheel or use staff
if grep -q '^staff:' /etc/group; then
    group="staff"
else
    group="wheel"
fi

# invoke insmod with all arguments we got
# and use a pathname, as insmod doesn't look in . by default
insmod ./$module.ko $* || exit 1

# retrieve major number
major=$(awk "\$2==\"$module\" {print \$1}" /proc/devices)

# Remove stale nodes and replace them, then give gid and perms
# Usually the script is shorter, it's scull that has several devices in it.

rm -f /dev/${device}[0-3]
mknod /dev/${device}0 c $major 0
mknod /dev/${device}1 c $major 1
mknod /dev/${device}2 c $major 2
mknod /dev/${device}3 c $major 3
ln -sf ${device}0 /dev/${device}
chgrp $group /dev/${device}[0-3] 
chmod $mode  /dev/${device}[0-3]
```
`第5行`:664表示只有owner,group才有寫入的權限，others只有讀取的權限。[檔案權限](https://shian420.pixnet.net/blog/post/344938711-%5Blinux%5D-chmod-%E6%AA%94%E6%A1%88%E6%AC%8A%E9%99%90%E5%A4%A7%E7%B5%B1%E6%95%B4!)

`第15行`:載入module`insomd ./$module.ko`，因為insmod不會尋找當時工作目錄所以模組檔名之前要加上`./`，並利用`$*`傳入所有命令列的所有引數。最後有一個`||`的操作，如果載入不成功就會exit。

`第18行`:[awk類似grep](https://ithelp.ithome.com.tw/articles/10136126)， /proc/devices的第一欄($1)事裝置的主編號。第二攔($2)是裝置名稱。

`第23行`:移除前次裝載所留下來的節點。

`第7~11行`:設定節點的擁有者與群組，有些系統使用wheel群組，但有些系統使用staff群組，所以需要用`if grep -q '^staff:' /etc/group`來判斷。其中`grep -q '^staff:`的`^staff:`代表只搜尋行首的字元，staff:是要搜尋的字元,並且是要在該行的行首。

`第29行`:利用[chgrp](https://www.itread01.com/content/1523008588.html)，更改`/dev/${device}[0-3]`的屬主，改成`$group`。讓`$group`的成員也有權寫入scull裝置。

`第30行`:利用[chmod](https://www.itread01.com/content/1523008588.html)，更改`/dev/${device}[0-3]`的九個屬性，改成`$mode`代表只有owner,group才有寫入的權限，others只有讀取的權限。
[如號撰寫shell script](https://blog.techbridge.cc/2019/11/15/linux-shell-script-tutorial/)
#### 取得裝置編號
==當驅動程式設定字元裝置時，要做的第一件事事先取得一個或多個裝置編號==
```cpp=
int scull_init_module(void)
{
	int result, i;
	dev_t dev = 0;

	/*
	 * Get a range of minor numbers to work with, asking for a dynamic
	 * major unless directed otherwise at load time.
	 */
	/*不管是靜態/動態allocate裝置編號，只要一allocate完後就可以在/proc/devices中找到scull所被分配到的主編號*/
    if (scull_major) {
		dev = MKDEV(scull_major, scull_minor);
		/*使用靜態分配，所以必須事先知道major*/
        result = register_chrdev_region(dev, scull_nr_devs, "scull");
	} else {
        /*使用動態分配*/
		result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs,
				"scull");
		scull_major = MAJOR(dev);
	}
	if (result < 0) {
		printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
		return result;
	}
    /*...*/
}
```



#### 重要的資料結構
驅動程式中的基本操作，多半涉及kernel的3種重要的資料結構，分別是`file_operations`,`file`,`inode`。

##### file_operatoins
定義在`<linux/fs.h>`，是一個操作檔案方法的集合。成員多是function pointer，所以file_operations這個資料結構有點像是工具箱，每個工頭(file)的工具相當中的工具部會完全相同，透過這些工具箱的內容，我們就可以知道這個工頭會哪一些東西。對於每個已經開啟的檔案（裝置）會以file結構表示，核心會各自賦予一組專屬的作業函式，也就是==file結構中的f_op會指向該檔案的作業函式的集合file_operations結構==。因此在對該檔案（裝置）呼叫system call時，會有專屬於該檔案（裝置）作業函式來對接。

##### file
定義在`<linux/fs.h>`的struct file，==file與user space當中的FILE指標毫無關西==，FILE是定義於C函式庫，不可能出現在kernel當中的程式碼中，而file結構也不會出現在user space的應用程式裡。
file結構代表已開啟的檔案(open file)，這不是驅動程式的專利，對於系統上每個已經開啟的檔案，在kernel space都有一個對應的struct file。每一個file結構都是kernel在收到open()系統呼叫時自動成立的，然後在最後一次close()系統呼叫返回。也就是沒有任何process持有file結構的指標時，kernel才會釋放file結構。
file struct成員當中最重要的就是 `struct file_operations *f_op`，他指向了上述的file_operations。

##### inode
Kernel內部使用inode結構來代表檔案，它不同於file結構，file結構是代表以開啟的檔案的FD(file descriptor)。如果同一個檔案配重複開啟多次，這樣的話kernel會有多個代表FD的file結構，但是他們==都指向同一個inode結構==。indoe結構中我們只在意
*    dev_t i_rdev
對於代表裝置檔的inodes，此欄位含有實際的裝置編號。
    ==盡量使用以下macro，避免直接操作i_rdev==
    *`unsigned int iminor(struct inode *inode);`
    *`unsigned int imajor(struct inode *inode);`
*    struct cdev *i_cdev
kernel內部用來表示字元裝置的結構，對於代表字元裝置的inode，此欄位含有一個指向該結構的指標。

#### 註冊字元裝置
##### struct cdev
是裝置與核心之間的介面。由於我們希望將struct cdev(linux/cdev.h)這個結構
![](https://i.imgur.com/FOEW25z.png)
嵌入到我們所定義的結構（scull_dev)之中，
![](https://i.imgur.com/eVxm9TN.png)
因此要使用`cdev_add`初始化cdev的結構，並將cdev結構中的`*ops`指向我們所定義的file_operations(scull_fops)。

---

#### 檔案構想
##### step1: 定義檔案的內容
```cpp=
struct scullc_dev {
	void **data;
	struct scullc_dev *next;  /* next listitem */
	int quantum;              /* the current allocation size */
	int qset;                 /* the current array size */
	size_t size;              /* 32-bit will suffice */
	struct mutex lock;     /* Mutual exclusion */
	struct cdev cdev;
};
```
##### step2: 定義檔案支援的函式
```cpp=
struct file_operations scullc_fops = {
	.owner =     THIS_MODULE,
	.llseek =    scullc_llseek,
	.read =	     scullc_read,
	.write =     scullc_write,
	.open =	     scullc_open,
	.release =   scullc_release,
};
```
##### step3: 定義函式
*    scullc_read, scullc_write在使用裝置時，==皆需要取得lock==。
*    scullc_open的主要功能就是透過inode中的i_cdev，取得該裝置的scullc_dev，並將scullc_dev存入filp->private_data。
##### step4: 初使化檔案的內容
```cpp=
int scullc_init(void)
{
	int result, i;
	dev_t dev = MKDEV(scullc_major, 0);
	
	/*
	 * Register your major, and accept a dynamic number.
	 */
	if (scullc_major) /*靜態分配裝置編號*/
		result = register_chrdev_region(dev, scullc_devs, "scullc");
        /*
         * scullc是決定/peoc/devices中獲得此編號範圍的裝置之名稱
         * scullc_devs是你想要申請的連續裝置編號的總數
         * */
	else { /*動態配置裝置編號*/
		result = alloc_chrdev_region(&dev, 0, scullc_devs, "scullc");
		scullc_major = MAJOR(dev);
	}
	if (result < 0)
		return result;
        
    /*scullc_devices指向由scullc_devs個scullc_dev所組成的陣列*/
	scullc_devices = kmalloc(scullc_devs*sizeof (struct scullc_dev), GFP_KERNEL);
	if (!scullc_devices) {
		result = -ENOMEM;
		goto fail_malloc;
	}
	memset(scullc_devices, 0, scullc_devs*sizeof (struct scullc_dev));
    /*初使化scullc_dev的成員*/
	for (i = 0; i < scullc_devs; i++) {
		scullc_devices[i].quantum = scullc_quantum;
		scullc_devices[i].qset = scullc_qset;
		mutex_init (&scullc_devices[i].lock);
		scullc_setup_cdev(scullc_devices + i, i); /*將由linux/cdev.h.h中定義的cdev嵌入scullc_dev中*/
	}

	scullc_cache = kmem_cache_create("scullc", scullc_quantum,
			0, SLAB_HWCACHE_ALIGN, NULL); /* no ctor/dtor */
	if (!scullc_cache) {
		scullc_cleanup();
		return -ENOMEM;
	}

	return 0; /* succeed */

  fail_malloc:
	unregister_chrdev_region(dev, scullc_devs);
	return result;
}

```
[Kmalloc VS kmem_cache_alloc](https://titanwolf.org/Network/Articles/Article?AID=08762a07-f3e2-4f50-bf30-59511e100efa#gsc.tab=0)
[Difference between kmalloc and kmem_cache_alloc](https://stackoverflow.com/questions/22370102/difference-between-kmalloc-and-kmem-cache-alloc)
##### step5: 將由linux/cdev.h.h中定義的cdev嵌入scullc_dev中
補充：
<linux/cdev.h>
```cpp=
struct cdev {
    ...
    struct module *owner;
    const struct file_operations *ops;
    ...
}
```
```cpp=
static void scullc_setup_cdev(struct scullc_dev *dev, int index)
{
	int err, devno = MKDEV(scullc_major, index);
    
	cdev_init(&dev->cdev, &scullc_fops);//將cdev中的ops成員指向scullc_fops，這樣就可以調用此檔案支援的函式
	dev->cdev.owner = THIS_MODULE;
	err = cdev_add (&dev->cdev, devno, 1);//準備好cdev結構之後，以cdev_add()將它加入核心，也就是註冊字元裝置。
	/* Fail gracefully if need be */
	if (err)
		printk(KERN_NOTICE "Error %d adding scull%d", err, index);
}
```
==當cdev_add()返回時，表示裝置已經生效了，核心隨時都有可能呼叫你的作業方法==。因此，除非驅動程式已經完全準備妥當，而且目標裝置也準備好開始運作，否則不應該呼叫cdev_add()。

##### step6: 透過mknod將裝置名稱加入 /dev

利用靜態/動態分配裝置編號後，使用mknod將裝置名稱加入/dev中，使用者可以透過裝置名稱來訪問使用裝置。
```cpp=
/*取得利用靜態/動態分配裝置編號後，已經分配到的主編號*/
major=`cat /proc/devices | awk "\\$2==\"$module\" {print \\$1}"`

/*major後面的0,1,2,3皆為次編號*/
mknod /dev/${device}0 c $major 0
mknod /dev/${device}1 c $major 1
mknod /dev/${device}2 c $major 2
mknod /dev/${device}3 c $major 3
ln -sf ${device}0  /dev/${device}

# give appropriate group/permissions
chgrp $group /dev/${device}[0-3]
chmod $mode  /dev/${device}[0-3]
```




---
#### 載入scull模組

此時的模組
![](https://i.imgur.com/UlxGleY.png)

/proc/devices 的內容
![](https://i.imgur.com/RDunuaB.png)

/dve 的內容
![](https://i.imgur.com/hAtA5cT.png)


#### 測試scullc模組

example.c
```cpp=
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <linux/fcntl.h>

int main()
{
 int fd;
 char buf[]="this is a example for character devices driver by Liao712148!";//寫入scullc0裝置的內容

 char buf_read[4096];//scullc0裝置的內容讀入到該buf中

 
 if((fd=open("/dev/scullc0",O_RDWR))==-1)//使用者透過/dev中的裝置名稱（scullc0)開啟scullc0裝置

  printf("open scullc0 WRONG！\n");
 else
  printf("open scullc0 SUCCESS!\n");
  
 printf("buf is %s\n",buf);

 write(fd,buf,sizeof(buf));//把buf中的內容寫入scullc0裝置

 
 lseek(fd,0,SEEK_SET);//把檔案指標重新定位到檔案開始的位置

 
 read(fd,buf_read,sizeof(buf));//把scullc0裝置中的內容讀入到buf_read中

 
 printf("buf_read is %s\n",buf_read);
 
 return 0;
}

```

輸出結果：
![](https://i.imgur.com/Nm7n5kA.png)

#### 退出scullc模組

![](https://i.imgur.com/vM2sDQX.png)
