互斥鎖可以說是“量值” 為 1 的
信號量, 最終實現的效果相同, 既然有了信號量, 那為什么還要有互斥鎖呢? 這就是我們這里需要了解并掌握的
文章目錄
- 參考資料
- 互斥鎖的介紹
- 互斥鎖結構體 - mutex
- 互斥鎖 API
- 互斥鎖實驗
- 源碼程序-mutex.c
- 部分源碼解讀
- 編譯腳本 Makefile
- 測試程序 app.c
- 準備測試命令和測試腳本-app.sh
- 測試命令
- 測試腳本
- 加載驅動 insmod - 查看 dev 下生成的設備
- 測試驗證互斥鎖程序
- 直接命令后臺驗證
- 腳本批量執行后臺任務 測試驗證
- 總結
- 互斥鎖與信號量的區別與聯系
- 基本概念對比
- 關鍵區別詳解
- 所有權機制
- 計數方式
- 性能特點
- 聯系與共同點
- 使用場景建議
- 使用互斥鎖的情況
- 使用信號量的情況
- 選擇指南
- 優先使用互斥鎖
- 考慮使用信號量
參考資料
前面了解了原子操作和自旋鎖,當然還有之前的字符設備相關操作,前面基礎知識還是需要重點掌握的,才能將知識點串聯起來:
接下來還是以前面字符設備 動態參數傳遞實驗為基礎,打開訪問字符設備實驗。 所以以前知識點 建議了解
在字符設備這塊內容,所有知識點都是串聯起來的,需要整體來理解,缺一不可,建議多了解一下基礎知識
驅動-申請字符設備號
驅動-注冊字符設備
驅動-創建設備節點
驅動-字符設備驅動框架
驅動-雜項設備
驅動-內核空間和用戶空間數據交換
驅動-文件私有數據
Linux驅動之 原子操作
Linux驅動—原子操作
驅動-自旋鎖
驅動-自旋鎖死鎖
驅動-信號量
互斥鎖的介紹
-
將信號量量值設置為 1, 最終實現的就是互斥效果, 這里要了解的互斥鎖功能相同, 雖然兩者功能相同但是具體的實現方式是不同的, 但是使用互斥鎖效率更高、更簡潔, 所以如果使用到的信號量“量值”為 1,一般將其修改為使用互斥鎖實現。當有多個線程幾乎同時修改某一個共享數據的時候, 需要進行同步控制。線程同步能夠保證多個線程安全訪問競爭資源, 最簡單的同步機制是引入互斥鎖。
-
互斥鎖為資源引入一個狀態: 鎖定或者非鎖定。 某個線程要更改共享數據時, 先將其鎖定, 此時資源的狀態為“鎖定” , 其他線程不能更改;直到該線程釋放資源, 將資源的狀態變成“非鎖定” , 其他的線程才能再次鎖定該資源。 互斥鎖保證了每次只有一個線程進行寫入操作,從而保證了多線程情況下數據的正確性, 能夠保證多個線程訪問共享數據不會出現資源競爭及數據錯誤
互斥鎖結構體 - mutex
struct mutex {atomic_t count; // 鎖計數器:1-未鎖,0-已鎖,負值-有等待者spinlock_t wait_lock; // 保護等待隊列的自旋鎖struct list_head wait_list; // 等待該鎖的進程隊列
};
互斥鎖 API
互斥鎖實驗
源碼程序-mutex.c
這個源碼程序,用到的還是訪問字符設備的最基本內容來講解,另外添加了 互斥鎖api 來規避并發和競爭
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/delay.h>
#include <linux/atomic.h>
#include <linux/errno.h>
#include <linux/semaphore.h>
#include <linux/mutex.h>struct mutex mutex_test;//定義mutex類型的互斥鎖結構體變量mutex_teststatic int open_test(struct inode *inode,struct file *file){printk("\n this is open_test \n");mutex_lock(&mutex_test);//互斥鎖加鎖return 0;};static ssize_t read_test(struct file *file,char __user *ubuf,size_t len,loff_t *off)
{int ret;char kbuf[10] = "topeet";//定義char類型字符串變量kbufprintk("\nthis is read_test \n");ret = copy_to_user(ubuf,kbuf,strlen(kbuf));//使用copy_to_user接收用戶空間傳遞的數據if (ret != 0){printk("copy_to_user is error \n");}printk("copy_to_user is ok \n");return 0;
}
static char kbuf[10] = {0};//定義char類型字符串全局變量kbuf
static ssize_t write_test(struct file *file,const char __user *ubuf,size_t len,loff_t *off)
{int ret;ret = copy_from_user(kbuf,ubuf,len);//使用copy_from_user接收用戶空間傳遞的數據if (ret != 0){printk("copy_from_user is error\n");}if(strcmp(kbuf,"topeet") == 0 ){//如果傳遞的kbuf是topeet就睡眠四秒鐘ssleep(4);}else if(strcmp(kbuf,"itop") == 0){//如果傳遞的kbuf是itop就睡眠兩秒鐘ssleep(2);}printk("copy_from_user buf is %s \n",kbuf);return 0;
}
static int release_test(struct inode *inode,struct file *file)
{printk("\nthis is release_test \n");mutex_unlock(&mutex_test);//互斥鎖解鎖return 0;
}struct chrdev_test
{dev_t dev_num; //定義dev_t類型變量來表示設備號int major,minor; //定義int 類型的主設備號和次設備號struct cdev cdev_test; //定義字符設備struct class *class_test; //定義結構體變量class 類
};struct chrdev_test dev1; //創建chardev_test類型結構體變量static struct file_operations fops_test = {.owner=THIS_MODULE,//將owner字段指向本模塊,可以避免在模塊的操作正在被使用時卸載該模塊.open = open_test,//將open字段指向chrdev_open(...)函數.read = read_test,//將open字段指向chrdev_read(...)函數.write = write_test,//將open字段指向chrdev_write(...)函數.release = release_test,//將open字段指向chrdev_release(...)函數
};//定義file_operations結構體類型的變量cdev_test_opsstatic int __init chrdev_fops_init(void)//驅動入口函數
{mutex_init(&mutex_test);//對互斥體進行初始化if(alloc_chrdev_region(&dev1.dev_num,0,1,"chrdev_name") < 0){printk("alloc_chrdev_region is error\n");} printk("alloc_chrdev_region is ok\n");dev1.major=MAJOR(dev1.dev_num);//通過MAJOR()函數進行主設備號獲取dev1.minor=MINOR(dev1.dev_num);//通過MINOR()函數進行次設備號獲取printk("major is %d\n",dev1.major);printk("minor is %d\n",dev1.minor);使用cdev_init()函數初始化cdev_test結構體,并鏈接到cdev_test_ops結構體cdev_init(&dev1.cdev_test,&fops_test);dev1.cdev_test.owner = THIS_MODULE;//將owner字段指向本模塊,可以避免在模塊的操作正在被使用時卸載該模塊 cdev_add(&dev1.cdev_test,dev1.dev_num,1);printk("cdev_add is ok\n");dev1.class_test = class_create(THIS_MODULE,"class_test");//使用class_create進行類的創建,類名稱為class_testdevice_create(dev1.class_test,NULL,dev1.dev_num,NULL,"device_test");//使用device_create進行設備的創建,設備名稱為device_testreturn 0;
}
static void __exit chrdev_fops_exit(void)//驅動出口函數
{cdev_del(&dev1.cdev_test);//使用cdev_del()函數進行字符設備的刪除unregister_chrdev_region(dev1.dev_num,1);//釋放字符驅動設備號 device_destroy(dev1.class_test,dev1.dev_num);//刪除創建的設備class_destroy(dev1.class_test);//刪除創建的類printk("module exit \n");}
module_init(chrdev_fops_init);//注冊入口函數
module_exit(chrdev_fops_exit);//注冊出口函數
MODULE_LICENSE("GPL v2");//同意GPL開源協議
MODULE_AUTHOR("wang fang chen "); //作者信息
部分源碼解讀
字符設備操作這里不再贅述,重點看看互斥鎖怎么用的。
在之前學習過原子操作設置標志位, 在同一時間內只允許一個任務對共享資源進行訪問的方式所不
同, 這里將采用互斥鎖的方式避免競爭的產生。 由于互斥體在同一時間內只允許一個任務對共享資源進行, 所以除了在 atomic_init()函數內加入初始化互斥鎖函數之外,只需要在 open()函數中加入互斥鎖加鎖函數, 在 release()函數中加入互斥鎖解鎖函數即可
- 定義結構體 - mutex
struct mutex mutex_test;//定義mutex類型的互斥鎖結構體變量mutex_test
- 驅動入口函數 init 中初始化 互斥鎖結構體,設置值 - mutex_init
static int __init chrdev_fops_init(void)//驅動入口函數
{mutex_init(&mutex_test);//對互斥體進行初始化
...
}
- 在open 中加鎖 - mutex_lock
static int open_test(struct inode *inode,struct file *file){printk("\n this is open_test \n");mutex_lock(&mutex_test);//互斥鎖加鎖return 0;};
- 程序釋放資源時候,解鎖- mutex_unlock
static int release_test(struct inode *inode,struct file *file)
{printk("\nthis is release_test \n");mutex_unlock(&mutex_test);//互斥鎖解鎖return 0;
}
編譯腳本 Makefile
#!/bin/bash
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
obj-m += mutex.o
KDIR :=/home/wfc123/Linux/rk356x_linux/kernel
PWD ?= $(shell pwd)
all:make -C $(KDIR) M=$(PWD) modulesclean:make -C $(KDIR) M=$(PWD) clean
測試程序 app.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>int main(int argc, char *argv[])
{int fd; // 定義int類型的文件描述符char str1[10] = {0}; // 定義讀取緩沖區str1fd = open(argv[1], O_RDWR, 0666); // 調用open函數,打開輸入的第一個參數文件,權限為可讀可寫// fd=open("/dev/device_test",O_RDWR,0666);//調用open函數,打開輸入的第一個參數文件,權限為可讀可寫if (fd < 0){printf("open is error\n");return -1;}printf("open is ok\n");if (strcmp(argv[2], "topeet") == 0){write(fd, "topeet", sizeof(str1));}else if (strcmp(argv[2], "itop") == 0){write(fd, "itop", sizeof(str1));}close(fd); // 調用close函數,對取消文件描述符到文件的映射return 0;
}
編譯 測試程序 app
aarch64-linux-gnu-gcc -o app app.c -static
準備測試命令和測試腳本-app.sh
測試命令
同時后臺執行兩個命令
./app /dev/device_test topeet &
./app /dev/device_test itop
測試腳本
這里準備簽名驅動自選死鎖的腳本,方便看看 互斥鎖的作用和效果。 app.sh
[root@topeet:/mnt/sdcard]# cat app.sh#!/bin/bash
taskset -c 0 ./app /dev/device_test topeet &
taskset -c 1 ./app /dev/device_test topeet &
taskset -c 2 ./app /dev/device_test topeet &
taskset -c 3 ./app /dev/device_test topeet &
taskset -c 0 ./app /dev/device_test topeet &
taskset -c 1 ./app /dev/device_test topeet &
taskset -c 2 ./app /dev/device_test topeet &
taskset -c 3 ./app /dev/device_test topeet &
加載驅動 insmod - 查看 dev 下生成的設備
加載驅動后,看一下字符相關操作是否有相關打印,從結果上看打印OK,邏輯正常在走。
字符設備都已經生成了,說明測試程序沒有問題的。
測試驗證互斥鎖程序
直接命令后臺驗證
./app /dev/device_test topeet &
./app /dev/device_test itop
看實驗結果如下:文件操作是一個等著一個執行的呢
腳本批量執行后臺任務 測試驗證
實際結果是,打印一個接著一個打印,會按照程序里面的邏輯 等待幾秒,執行完后才會執行下一個任務命令。 而且最重要的是 這里用的是自旋鎖死鎖的 腳本來驗證,在互斥鎖這里不會死機。 這樣更方便理解 互斥鎖的原理了。
總結
- 互斥鎖也是解決并發、競爭問題的一種方案
- 淺顯的看: 互斥鎖原理就是一個全局的變量,類似于原子操作。會讓線程、進程去處理其它事情,不用想自旋鎖原地等待。大量頻繁使用會增加切換資源消耗。
互斥鎖與信號量的區別與聯系
基本概念對比
特性 | 互斥鎖(Mutex) | 內核態信號量(Semaphore) |
---|---|---|
本質 | 特殊的二進制信號量 | 更通用的同步機制 |
持有者 | 有明確的持有者(必須由獲取者釋放) | 無持有者概念 |
計數 | 只能是0或1(二進制) | 可以是任意正整數(計數信號量) |
性能 | 更高(優化過的實現) | 相對較低 |
優先級繼承 | 支持(防止優先級反轉) | 不支持 |
使用場景 | 短期臨界區保護 | 資源計數管理 |
關鍵區別詳解
所有權機制
互斥鎖具有嚴格的所有權概念:
只有鎖定mutex的線程才能解鎖它內核會跟蹤當前持有者這種設計有助于調試和死鎖檢測
- 信號量沒有所有權概念:
任何線程都可以對信號量執行up操作更靈活但也更容易出錯
計數方式
- 互斥鎖是二進制鎖:
只有鎖定/未鎖定兩種狀態一次只允許一個線程進入臨界區
- 信號量是計數信號量:
初始化時可設置任意正整數值允許多個線程同時訪問資源(當計數>1時)
性能特點
- 互斥鎖經過高度優化:
快速路徑(fast path)通常只需幾條原子指令在無競爭情況下性能接近無鎖
- 信號量開銷較大:
總是涉及上下文切換即使在無競爭情況下也需要更多操作
聯系與共同點
- 同步基礎:兩者都基于內核的等待隊列機制實現
- 睡眠特性:當資源不可用時,都會使調用者睡眠
- 不可中斷上下文使用:都不能在原子上下文(如中斷處理程序)中使用
- 解決競態條件:都可用于保護共享資源,防止競態條件
使用場景建議
使用互斥鎖的情況
- 需要嚴格互斥訪問的共享資源
- 臨界區執行時間較短
- 需要防止優先級反轉的實時應用
使用信號量的情況
- 需要限制并發訪問數量的資源
- 允許多個讀者同時訪問的情況
- 需要跨多個模塊釋放鎖的復雜場景
選擇指南
優先使用互斥鎖
- 只需要二進制鎖定
- 性能是關鍵考量
- 需要調試支持(如死鎖檢測)
- 在實時系統中需要優先級繼承
考慮使用信號量
- 需要計數功能
- 鎖定可能被不同模塊釋放
- 需要允許多個并發訪問(如讀者)