在计算机系统中,变量、中间数据一般存放在系统存储空间中,只有实际使用的时候才将他们从存储空间调入到中央处理器内部进行计算。通常存储空间分为两类:内部存储空间和外部存储空间。对于电脑来讲,内部存储空间就是电脑的内存,外部存储空间就是电脑的硬盘。而对于单片机来讲,内部存储就是 RAM ,随机存储器。外部存储可以理解为 flash ,掉电不丢失。该篇文章的主题,内存管理,主要讨论的是关于 RAM 的管理。
针对于 Cortex M3 内核的单片机的详细内存分配可以参照笔者的这篇文章 STM32 内存分配解析及变量的存储位置 ,在这里不进行赘述,简单的进行划分一下,大致可以分为三个部分:静态区,栈,堆。
静态区:保存全局变量和 static 变量(包括由 static 修饰的全局变量和局部变量)。静态区的内容在总个程序的生命周期内都存在,由编译器在编译的时候进行分配。
栈:保存局部变量。栈上的内容只在函数的范围内存在,当函数运行结束,这些内容也会自动被销毁。其特点是效率高,但空间大小有限。
堆:由 malloc 函数分配的内存。其生命周期由 free 决定,在没有释放之前一直存下,直到程序结束。
涉及到动态内存管理时,会触及到两个概念,一个就是内存碎片另一个就是内存泄漏,下面分别阐述着两个概念。
假设我现在有 16 个字节的空闲内存,如下图所示:
现在我使用 malloc 分配了四次内存,然后这 16 个字节的内存变成了这样:
然后,又使用 free 释放了三次内存,释放之后的内存空间是这样的:
在没有 MMU 的情况下,现在我准备用 malloc 一次性分配 12 个字节的内存空间,虽然上述 16 个字节的内存空间还剩下 13 个字节,但是却因为内存不是连续的,因此是不能够进行分配的,这也就是出现内存碎片的原因了。
内存泄漏产生的原因是当分配时的内存已经不再使用了,但是却没有被释放掉,这个时候,导致内存不够用,这对于嵌入式设备这种内存极其有限的对象来说是极其有害的。因此,在使用 malloc
时,要搭配着 free
来进行使用。
静态区,栈我们我们在编写程序的时候都会涉及到,定义一个全局变量,就存放在了静态区,在函数内部定义了一个局部变量,就存放在了栈,那堆呢?堆什么时候会使用到呢?假设现在有这样一个程序。
int main(void){
char *buffer[3] = {NULL};
char *string1 = "hello";
char *string2 = "word";
char *string3 = "wenzi";
buffer[0] = (char *)malloc(strlen(string1) + 1);
if (buffer[0] != NULL)
strcpy(buffer[0],string1);
buffer[1] = (char *)malloc(strlen(string2) + 1);
if (buffer[1] != NULL)
strcpy(buffer[1],string2);
buffer[2] = (char *)malloc(strlen(string3) + 1);
if (buffer[2] != NULL)
strcpy(buffer[2],string3);
}
可以看到上述代码的意思是将string1
、string2
、string3
三个字符串复制到 buffer 所在内存位置,但是这个时候,如果不给数组的元素分配一定大小的内存,那么可能就放不下拷贝进去的字符串,因此在往里面拷贝字符串时,应该提前开辟出一段内存空间出来,这个时候,就需要使用到 malloc 来进行内存分配,当然所对应的,当这个数组使用完之后,需要使用 free
来将分配的内存释放掉,否则就会造成内存泄漏
。
在上述介绍的分配内存中,都是使用 malloc
来进行分配内存,然后使用 free
来进行释放内存,但是针对于单片机 RAM 如此紧缺的设备来讲,使用 C 标准库中的内存管理函数是不恰当的,存在着许多弊端,主要有以下几点:
他们的实现可能非常大,占据了相当大的一块代码空间
这两个函数会使得链接器配置得复杂
如果允许堆空间的生长方向覆盖其他变量的内存,他们会成为 debug 的灾难
基于此,正点原子的例程中给出了一种内存管理的方法:分块式内存管理,实现原理如下图所示:
简单说明一下,分块式内存管理由内存池和内存管理表构成。内存池被等分为 n 块,对应的内存管理表,大小也为 n。内存管理表的每一项对应着内存池的一块内存。之所以有内存表项的存在,是因为需要通过内存表项表征当前内存块有没有被占用,如果对应的内存块没有被占用,那么该表项值就为 0 ,非 0 表示该内存块已经被占用。如果某项值为 10,那么说明本项对应的内存块在内,总共分配了 10 个内存块给外部的某个指针。
当指针 p 调用 malloc 申请内存的时候,先判断 p 要分配的内存块数(m),然后从第 n 项开始,向下查找,直到找到 m 块连续的空内存块(即对应内存管理表项为 0),然后将这 m 个内存管理表项的值都设置为 m(标记被占用),最后,把最后的这个空内存块的地址返回指针 p,完成一次分配。注意,如果当内存不够的时候(找到最后也没找到连续的 m 块空闲内存),则返回 NULL 给 p,表示分配失败。基于此原理,我们来完成内存分配函数。
首先我们需要定义内存池的大小和内存表的大小:
#define MEM1_BLOCK_SIZE 32 //内存块大小为32字节
#define MEM1_MAX_SIZE 10*1024 //最大管理内存 10K
#define MEM1_ALLOC_TABLE_SIZE MEM1_MAX_SIZE/MEM1_BLOCK_SIZE //内存表大小
上述中内存表的大小直接用内存池的大小除以内存块的大小是因为内存管理表和内存块一一对应的,内存块的数量也就等于内存池中内存块的数量。
有了内存池和内存管理表的大小,那么就可以定义内存池和内存管理表了,定义如下所示:
//内存池(32字节对齐)
__align(32) uint8_t mem1base[MEM1_MAX_SIZE]; //内部SRAM内存
//内存管理表
uint16_t mem1mapbase[MEM1_ALLOC_TABLE_SIZE]; //内部SRAM内存池MAP
//内存管理参数
const uint32_t memtblsize = MEM1_ALLOC_TABLE_SIZE; //内存表大小
const uint32_t memblksize = MEM1_BLOCK_SIZE; //内存分块大小
const uint32_t memsize = MEM1_MAX_SIZE; //内存总大小
上述所定义的就是内存池和内存管理表的相关内容,关于内存池采用 32 个字节对齐是因为 内存块的大小是 32 字节,而且我们从这里也可以看到我们所定义的内存池本质就是一个全局变量的数组,这个数组在编译时,就被分配了一个固定大小的内存,然后我们会编写 malloc 函数往这个内存池中去分配内存,紧接着,为了使得程序更加简洁,我们创建一个结构体,用来存储内存管理的相关参数:
struct _m_mallco_dev
{
void (*init)(void); //初始化
uint8_t (*perused)(void); //内存使用率
uint8_t *membase; //内存池 管理SRAMBANK个区域的内存
uint16_t *memmap; //内存管理状态表
uint8_t memrdy; //内存管理是否就绪
}
可以看到这个结构体包含了两个函数指针,两个指针,以及一个普通变量。有了结构体类型之后,我们定义一个变量并初始化如下:
struct _m_mallco_dev mallco_dev=
{
my_mem_init, //内存初始化
my_mem_perused, //内存使用率
mem1base, //内存池
mem1mapbase, //内存管理状态表
0, //内存管理未就绪
};
可以看到对与初始化的结构体变量来说,两个函数指针,指向的分别是内存初始化和内存使用率函数,内存使用率函数不在这里阐述了,需要了解的可以在公众号底部回复 内存管理
获得内存管理源代码进行学习。这里阐述一下内存初始化,回顾我们之前定义的内存池,是一个全局变量的数组,因此,这里的初始化实际也就是对于全局数组进行赋 0 操作,代码如下所示:
void my_mem_init(void) {
mymemset(mallco_dev.memmap, 0,memtblsize*2);//内存状态表数据清零
mymemset(mallco_dev.membase, 0,memsize); //内存池所有数据清零
mallco_dev.memrdy = 1; //内存管理初始化OK
}
上述的 mymemset
函数也不在这里阐述了,可以自行阅读笔者在公众号后天给出的源代码,上述代码功能也就是对内存池和内存管理表进行赋 0 ,为什么赋 0 时内存管理表的大小要乘以 2 ,是因为内存管理表是的数据是 16 位的,而计算内存管理表的大小时所依据的是 8 位的内存池的数据。
有了初始化,我们就可以根据所要求获取的内存大小向内存池获取内存了,下面是内存分配的代码实现:
uint32_t my_mem_malloc(uint32_t size)
{
signed long offset=0;
uint32_t nmemb; //需要的内存块数
uint32_t cmemb = 0;//连续空内存块数
uint32_t i;
if (!mallco_dev.memrdy)
mallco_dev.init();//未初始化,先执行初始化
if (size == 0)
return 0XFFFFFFFF;//不需要分配
nmemb = size / memblksize; //获取需要分配的连续内存块数
if (size % memblksize)
nmemb ++;
for (offset = memtblsize-1; offset >= 0; offset--)//搜索整个内存控制区
{
if (!mallco_dev.memmap[offset])
cmemb++;//连续空内存块数增加
else
cmemb = 0; //连续内存块清零
if (cmemb == nmemb) //找到了连续nmemb个空内存块
{
for(i = 0; i //标注内存块非空
{
mallco_dev.memmap[offset+i] = nmemb;
}
return (offset*memblksize);//返回偏移地址
}
}
return 0XFFFFFFFF;//未找到符合分配条件的内存块
}
上述代码仔细阅读也不难理解,总体来说,分配的过程最开始是检查内存池是否已经初始化,如果没有初始化,那么就进行初始化,进一步地就检查所要分配的大小是否等于 0 ,如果等于0 ,那么就返回。接下来的就是根据要分配的内存大小来计算所要分配的内存块数,最后,所要分配的内存可能不足以需要一整个内存块了,但是不足的话仍旧以一个内存块来进行计算,紧接着,就开始从内存池的底部开始寻找空闲内存块,如果找到了,就将对应的内存管理表赋值成所要分配的内存块大小。最后,返回所分配的内存在内存池中的偏移。注意,到这里并没有结束,返回的只是偏移,并不是我们所需要的地址,因此,我们还需要如下所示的一个函数:
void *mymalloc(uint32_t size) {
uint32_t offset;
offset = my_mem_malloc(size);
if (offset == 0XFFFFFFFF)
return NULL;
else
return (void*)((uint32_t)mallco_dev.membase+offset);
}
上面这个函数就不在这里赘述了,其功能呢就是将在我们刚刚那个函数得到的偏移地址加上内存池所在的地址就得到了我们分配的那个内存的地址。
当 p 申请的内存用完,需要释放的时候,调用 free 函数实现。free 函数先判断 p 指向的内存地址所对应的内存块,然后找到对应的内存管理表项目,得到 p 所占用的内存块数目 m(内存管理表项目的值就是所分配内存块的数目),将这 m 个内存管理表项目的值都清零,标记释放,完成一次内存释放。这就是内存释放的原理,对应的代码如下所示:
uint8_t my_mem_free(uint32_t offset)
{
int i;
if (!mallco_dev.memrdy) //未初始化,先执行初始化
{
mallco_dev.init();
return 1; //未初始化
}
if (offset //偏移在内存池内.
{
int index = offset/memblksize; //偏移所在内存块号码
int nmemb = mallco_dev.memmap[index]; //内存块数量
for(i = 0; i //内存块清零
{
mallco_dev.memmap[index+i]=0;
}
return 0;
}
else
return 2;//偏移超区了.
}
通过上述代码我们也可以知道关于内存的释放只需要将其内存管理表的项置 0 就好,简而言之,我们需要找到需要释放的内存所在的地址,然后根据内存管理表的数值一次将内存管理表的值进行置 0 就完成了内存的释放,当然,上述代码也不是全部,释放前我们需要知道释放内存在内存池中的偏移,这部分代码如下所示:
void myfree(void *ptr) {
uint32_t offset;
if(ptr==NULL)return;//地址为0.
offset=(uint32_t)ptr-(uint32_t)mallco_dev.membase;
my_mem_free(offset); //释放内存
}
其中 ptr 就是要释放的内存的地址,然后在减去内存池所在的地址,就可以得到要释放的内存在内存池中的偏移。
上述就是关于在裸机上实现的一个内存管理,仔细来看实现原理其实挺简单,关于这个例子,笔者觉得也仅仅是提供了一个关于内存分配的一个思路,要真正的运用到实际中,还存在问题,在上述中的内存分配中,在进行分配时,当要分配的大小小于一个内存块的大小时,直接采用的是分配一个内存块的大小,而在例子中定义的内存块大小是 32 K ,也就是说如果分配的内存大小小于 32 K ,那就分配 32 K ,这样是极其浪费的。如果把内存块定义的太小,那么相应伴随的又是内存管理表数组的增大,也会增加对于 RAM 的消耗,所以总体来说上述的代码存在着一些不完善,但是对于学习来说是极好的~
概念Linux内核的信号量在概念和原理上和用户态的System V的IPC机制信号量是相同的,不过他绝不可能在内核之外使用,因此他和System V的IPC机制信号量毫不相干。
如果有一个任务想要获得已经被占用的信号量时,信号量会将其放入一个等待队列(它不是站在外面痴痴地等待而是将自己的名字写在任务队列中)然后让其睡眠。
当持有信号量的进程将信号释放后,处于等待队列中的一个任务将被唤醒(因为队列中可能不止一个任务),并让其获得信号量。这一点与自旋锁不同,处理器可以去执行其它代码。
应用场景由于争用信号量的进程在等待锁重新变为可用时会睡眠,所以信号量适用于锁会被长时间持有的情况;相反,锁被短时间持有时,使用信号量就不太适宜了,因为睡眠、维护等待队列以及唤醒所花费的开销可能比锁占用的全部时间表还要长。
举2个生活中的例子:
我们坐火车从南京到新疆需要2天的时间,这个'任务'特别的耗时,只能坐在车上等着车到站,但是我们没有必要一直睁着眼睛等,理想的情况就是我们上车就直接睡觉,醒来就到站(看过《异形》的读者会深有体会),这样从人(用户)的角度来说,体验是最好的,对比于进程,程序在等待一个耗时事件的时候,没有必须要一直占用CPU,可以暂停当前任务使其进入休眠状态,当等待的事件发生之后再由其他任务唤醒,类似于这种场景采用信号量比较合适。
我们有时候会等待电梯、洗手间,这种场景需要等待的时间并不是很多,如果我们还要找个地方睡一觉,然后等电梯到了或者洗手间可以用了再醒来,那很显然这也没有必要,我们只需要排好队,刷一刷抖音就可以了,对比于计算机程序,比如驱动在进入中断例程,在等待某个寄存器被置位,这种场景需要等待的时间往往很短暂,系统开销甚至远小于进入休眠的开销,所以这种场景采用自旋锁比较合适。
关于信号量和自旋锁,以及死锁问题,我们后面会再详细讨论。
使用方法一个任务要想访问共享资源,首先必须得到信号量,获取信号量的操作将把信号量的值减1,若当前信号量的值为负数,表明无法获得信号量,该任务必须挂起在 该信号量的等待队列等待该信号量可用;若当前信号量的值为非负数,表示能获得信号量,因而能即时访问被该信号量保护的共享资源。
当任务访问完被信号量保护的共享资源后,必须释放信号量,释放信号量通过把信号量的值加1实现,如果信号量的值为非正数,表明有任务等待当前信号量,因此他也唤醒所有等待该信号量的任务。
内核信号量的构成内核信号量类似于自旋锁,因为当锁关闭着时,它不允许内核控制路径继续进行。然而,当内核控制路径试图获取内核信号量锁保护的忙资源时,相应的进程就被挂起。只有在资源被释放时,进程才再次变为可运行。
只有可以睡眠的函数才能获取内核信号量;中断处理程序和可延迟函数都不能使用内核信号量。
内核信号量是struct semaphore类型的对象,在内核源码中位于include\linux\semaphore.h文件
struct semaphore{
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
}
成员 | 描述 |
---|---|
lock | 在2.6.33之后的版本,内核加入了raw_spin_lock系列,使用方法和spin_lock系列一模一样,只是参数spinlock_t变为了raw_spinlock_t |
count | 相当于信号量的值,大于0,资源空闲;等于0,资源忙,但没有进程等待这个保护的资源;小于0,资源不可用,并至少有一个进程等待资源 |
wait_list | 内核链表,当前获得信号量的任务会与该成员一起注册到等待的链表中 |
初始化
DECLARE_MUTEX(name)
该宏声明一个信号量name并初始化他的值为1,即声明一个互斥锁。
DECLARE_MUTEX_LOCKED(name)
该宏声明一个互斥锁name,但把他的初始值设置为0,即锁在创建时就处在已锁状态。因此对于这种锁,一般是先释放后获得。
void sema_init (struct semaphore *sem, int val);
该函用于数初始化设置信号量的初值,他设置信号量sem的值为val。
注意:
val设置为1说明只有一个持有者,这种信号量叫二值信号量或者叫互斥信号量。
我们还允许信号量可以有多个持有者,这种信号量叫计数信号量,在初始化时要说明最多允许有多少个持有者也可以把信号量中的val初始化为任意的正数值n,在这种情况下,最多有n个进程可以并发地访问这个资源。
void init_MUTEX (struct semaphore *sem);
该函数用于初始化一个互斥锁,即他把信号量sem的值设置为1。
void init_MUTEX_LOCKED (struct semaphore *sem);
该函数也用于初始化一个互斥锁,但他把信号量sem的值设置为0,即一开始就处在已锁状态。
PV操作void down(struct semaphore * sem);
该函数用于获得信号量sem,他会导致调用该函数的进程睡眠,因此不能在中断上下文(包括IRQ上下文和softirq上下文)使用该函数。该函数将把sem的值减1,如果信号量sem的值非负,就直接返回,否则调用者将被挂起,直到别的任务释放该信号量才能继续运行。
int down_interruptible(struct semaphore * sem);
该函数功能和down类似,不同之处为,down不会被信号(signal)打断,但down_interruptible能被信号打断,因此该函数有返回值来区分是正常返回还是被信号中断,如果返回0,表示获得信号量正常返回,如果被信号打断,返回-EINTR。
int down_trylock(struct semaphore * sem);
该函数试着获得信号量sem,如果能够即时获得,他就获得该信号量并返回0,否则,表示不能获得信号量sem,返回值为非0值。因此,他不会导致调用者睡眠,能在中断上下文使用。
int down_killable(struct semaphore *sem);
int down_timeout(struct semaphore *sem, long jiffies);
int down_timeout_interruptible(struct semaphore *sem, long jiffies);
void up(struct semaphore * sem);
该函数释放信号量sem,即把sem的值加1,如果sem的值为非正数,表明有任务等待该信号量,因此唤醒这些等待者。
int down_interruptible(struct semaphore *sem)
这个函数的功能就是获得信号量,如果得不到信号量就睡眠,此时没有信号打断,那么进入睡眠。但是在睡眠过程中可能被信号打断,打断之后返回-EINTR,主要用来进程间的互斥同步。
下面是该函数的注释:
/**
* down_interruptible - acquire the semaphore unless interrupted
* @sem: the semaphore to be acquired
*
* Attempts to acquire the semaphore. If no more tasks are allowed to
* acquire the semaphore, calling this function will put the task to sleep.
* If the sleep is interrupted by a signal, this function will return -EINTR.
* If the semaphore is successfully acquired, this function returns 0.
*/
一个进程在调用down_interruptible()之后&#xff0c;如果sem<0&#xff0c;那么就进入到可中断的睡眠状态并调度其它进程运行&#xff0c; 但是一旦该进程收到信号&#xff0c;那么就会从down_interruptible函数中返回。并标记错误号为:-EINTR。
一个形象的比喻&#xff1a;传入的信号量为1好比天亮&#xff0c;如果当前信号量为0&#xff0c;进程睡眠&#xff0c;直到(信号量为1)天亮才醒&#xff0c;但是可能中途有个闹铃(信号)把你闹醒。
又如&#xff1a;小强下午放学回家&#xff0c;回家了就要开始吃饭嘛&#xff0c;这时就会有两种情况&#xff1a;情况一&#xff1a;饭做好了&#xff0c;可以开始吃&#xff1b;情况二&#xff1a;当他到厨房去的时候发现妈妈还在做&#xff0c; 妈妈就对他说&#xff1a;“你先去睡会&#xff0c;待会做好了叫你。” 小强就答应去睡会&#xff0c;不过又说了一句&#xff1a;“睡的这段时间要是小红来找我玩&#xff0c;你可以叫醒我。” 小强就是down_interruptible&#xff0c;想吃饭就是获取信号量&#xff0c;睡觉对应这里的休眠&#xff0c;而小红来找我玩就是中断休眠。
使用可被中断的信号量版本的意思是&#xff0c;万一出现了semaphore的死锁&#xff0c;还有机会用ctrl&#43;c发出软中断&#xff0c;让等待这个内核驱动返回的用户态进程退出。而不是把整个系统都锁住了。在休眠时&#xff0c;能被中断信号终止&#xff0c;这个进程是可以接受中断信号的&#xff01;
比如你在命令行中输入# sleep 10000&#xff0c;按下ctrl &#43; c&#xff0c;就给上面的进程发送了进程终止信号。信号发送给用户空间&#xff0c;然后通过系统调用&#xff0c;会把这个信号传给递给驱动。信号只能发送给用户空间&#xff0c;无权直接发送给内核的&#xff0c;那1G的内核空间&#xff0c;我们是无法直接去操作的。
内核信号量的使用例程场景1
在驱动程序中&#xff0c;当多个线程同时访问相同的资源时(驱动中的全局变量时一种典型的共享资源)&#xff0c;可能会引发“竞态“&#xff0c;因此我们必须对共享资源进行并发控制。Linux内核中
解决并发控制的最常用方法是自旋锁与信号量(绝大多数时候作为互斥锁使用)。
场景2
有时候我们希望设备只能被一个进程打开&#xff0c;当设备被占用的时候&#xff0c;其他设备必须进入休眠。
信号处理示意图
如上图&#xff1a;
进程A首先通过open()打开设备文件&#xff0c;调用到内核的hello_open()&#xff0c;并调用down_interruptible()&#xff0c;因为此时信号量没有被占用&#xff0c;所以进程A可以获得信号量&#xff1b;
进程A获得信号量之后继续处理原有任务&#xff0c;此时进程B也要通过open()打开设备文件&#xff0c;同样调用内核函数hello_open(),但此时信号量获取不到&#xff0c;于是进程B被阻塞&#xff1b;
进程A任务执行完毕&#xff0c;关闭设备文件&#xff0c;并通过up()释放信号量&#xff0c;于是进程B被唤醒&#xff0c;并得以继续执行剩下的任务&#xff0c;
进程B执行完任务&#xff0c;释放设备文件&#xff0c;通过up()释放信号量
代码如下&#xff1a;
#include
#include
#include
#include
#include
#include
#include
static int major &#61; 250;
static int minor &#61; 0;
static dev_t devno;
static struct cdev cdev;
static struct class *cls;
static struct device *test_device;
static struct semaphore sem;
static int hello_open (struct inode *inode, struct file *filep){
if(down_interruptible(&sem))//p
{
return -ERESTARTSYS;
}
return 0;
}
static int hello_release (struct inode *inode, struct file *filep){
up(&sem);//v
return 0;
}
static struct file_operations hello_ops &#61;
{
.open &#61; hello_open,
.release &#61; hello_release,
};
static int hello_init(void){
int result;
int error;
printk("hello_init \n");
result &#61; register_chrdev( major, "hello", &hello_ops);
if(result 0)
{
printk("register_chrdev fail \n");
return result;
}
devno &#61; MKDEV(major,minor);
cls &#61; class_create(THIS_MODULE,"helloclass");
if(IS_ERR(cls))
{
unregister_chrdev(major,"hello");
return result;
}
test_device &#61; device_create(cls,NULL,devno,NULL,"test");
if(IS_ERR(test_device ))
{
class_destroy(cls);
unregister_chrdev(major,"hello");
return result;
}
sem_init(&sem,1);
return 0;
}
static void hello_exit(void){
printk("hello_exit \n");
device_destroy(cls,devno);
class_destroy(cls);
unregister_chrdev(major,"hello");
return;
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("daniel.peng")&#xff1b;
测试程序 test.c
#include
#include
#include
#include
main()
{
int fd;
printf("before open\n ");
fd &#61; open("/dev/test",O_RDWR); //原子变量 0
if(fd<0)
{
perror("open fail \n");
return;
}
printf("open ok ,sleep......\n ");
sleep(20);
printf("wake up from sleep!\n ");
close(fd); //加为1
}
编译步骤
1 make 生成 hello.ko
2 gcc test.c -o a
3 gcc test.c -o b
测试步骤
insmod hello.ko
可见进程A成功打开设备&#xff0c;在进程A sleep期间会一直占有该字符设备&#xff0c;进程B由于无法获得信号量&#xff0c;进入休闲&#xff0c;结合代码可知&#xff0c;进程B被阻塞在函数open()中。
跟自旋锁一样&#xff0c;信号量也有区分读-写信号量之分。
如果一个读写信号量当前没有被写者拥有并且也没有写者等待读者释放信号量&#xff0c;那么任何读者都可以成功获得该读写信号量&#xff1b;否则&#xff0c;读者必须被挂起直到写者释放该信号量。如果一个读写信号量当前没有被读者或写者拥有并且也没有写者等待该信号量&#xff0c;那么一个写者可以成功获得该读写信号量&#xff0c;否则写者将被挂起&#xff0c;直到没有任何访问者。因此&#xff0c;写者是排他性的&#xff0c;独占性的。
读写信号量有两种实现&#xff0c;一种是通用的&#xff0c;不依赖于硬件架构&#xff0c;因此&#xff0c;增加新的架构不需要重新实现它&#xff0c;但缺点是性能低&#xff0c;获得和释放读写信号量的开销大&#xff1b;另一种是架构相关的&#xff0c;因此性能高&#xff0c;获取和释放读写信号量的开销小&#xff0c;但增加新的架构需要重新实现。在内核配置时&#xff0c;可以通过选项去控制使用哪一种实现。
读写信号量的相关API&#xff1a;
DECLARE_RWSEM(name)
该宏声明一个读写信号量name并对其进行初始化。
void init_rwsem(struct rw_semaphore *sem);
该函数对读写信号量sem进行初始化。
void down_read(struct rw_semaphore *sem);
读者调用该函数来得到读写信号量sem。该函数会导致调用者睡眠&#xff0c;因此只能在进程上下文使用。
int down_read_trylock(struct rw_semaphore *sem);
该函数类似于down_read&#xff0c;只是它不会导致调用者睡眠。它尽力得到读写信号量sem&#xff0c;如果能够立即得到&#xff0c;它就得到该读写信号量&#xff0c;并且返回1&#xff0c;否则表示不能立刻得到该信号量&#xff0c;返回0。因此&#xff0c;它也可以在中断上下文使用。
void down_write(struct rw_semaphore *sem);
写者使用该函数来得到读写信号量sem&#xff0c;它也会导致调用者睡眠&#xff0c;因此只能在进程上下文使用。
int down_write_trylock(struct rw_semaphore *sem);
该函数类似于down_write&#xff0c;只是它不会导致调用者睡眠。该函数尽力得到读写信号量&#xff0c;如果能够立刻获得&#xff0c;就获得该读写信号量并且返回1&#xff0c;否则表示无法立刻获得&#xff0c;返回0。它可以在中断上下文使用。
void up_read(struct rw_semaphore *sem);
读者使用该函数释放读写信号量sem。它与down_read或down_read_trylock配对使用。
如果down_read_trylock返回0&#xff0c;不需要调用up_read来释放读写信号量&#xff0c;因为根本就没有获得信号量。
void up_write(struct rw_semaphore *sem);
写者调用该函数释放信号量sem。它与down_write或down_write_trylock配对使用。如果down_write_trylock返回0&#xff0c;不需要调用up_write&#xff0c;因为返回0表示没有获得该读写信号量。
void downgrade_write(struct rw_semaphore *sem);
该函数用于把写者降级为读者&#xff0c;这有时是必要的。因为写者是排他性的&#xff0c;因此在写者保持读写信号量期间&#xff0c;任何读者或写者都将无法访问该读写信号量保护的共享资源&#xff0c;对于那些当前条件下不需要写访问的写者&#xff0c;降级为读者将&#xff0c;使得等待访问的读者能够立刻访问&#xff0c;从而增加了并发性&#xff0c;提高了效率。
读写信号量适于在读多写少的情况下使用&#xff0c;在linux内核中对进程的内存映像描述结构的访问就使用了读写信号量进行保护。
-THE END-
推荐阅读
【1】SPI转can芯片CSM300详解、Linux驱动移植调试笔记 必读【2】到底什么是Cortex、ARMv8、arm架构、ARM指令集、soc&#xff1f;一文帮你梳理基础概念【科普】 必读【3】搞懂进程组、会话、控制终端关系&#xff0c;才能明白守护进程干嘛的&#xff1f;【4】快速掌握TCP协议【5】Linux库概念&#xff0c;动态库和静态库的制作&#xff0c;如何移植第三方库【6】I2C基础知识入门 必读本公众号全部原创干货已整理成一个目录&#xff0c;点击「干货」即可获得。
后台回复「进群」&#xff0c;即可加入技术交流群&#xff0c;进群福利&#xff1a;免费赠送Linux学习资料。