《网蜂A8实战演练》——4.高级字符驱动

第6章  Linux 高级字符设备驱动


6.1  高级字符设备驱动
学习完第四第五章,是不是感觉字符设备驱动已经没什么新鲜感了,好像感觉挺简单的,感觉驱动已经趋向完美了。大家觉得前面的驱动是否已经完美?思考 1 分钟,zzzzZZZZZ......
别睡着啦,完美吗?好像完美了,又好像不完美。但又不知道哪里还不足,如果你有这种含糊的感觉的话,那么证明你是驱动初学者。没关系,接下来的高级字符设备驱动,就是要告诉你,前面写的驱动,还有很大缺陷。然后你可能会反驳,这世上本来就没有完美的东西......
猜想一:
那么有没有这样一种情况,有些设备并不是一直产生数据,它只是偶尔有数据产生,偶尔又没有数据产生,答案肯定是有的。那之前我们的应用程序是一直死循环的读取数据,不管设备有没有数据。那么如何改进呢?很自然的想到,当有数据的时候,我们才去读它,没有数据的时候我们读它干啥?岂不浪费劳动
力?
猜想二:
之前,一直都是应用程序主动去读取驱动里的数据,那有没有一种情况,当驱动程序有数据时,主动去告诉应用程序,告诉它,有数据了,你赶紧来读吧。答案当然是有的,这种情况在 linux 里的专业术语就叫异步通知。
猜想三:
前面的驱动测试实验,我们说过,有 BUG。什么 BUG 呢?就是按键并不是以按下/松开成对出现的。换句话说,当你只按下/松开一次 S2 时,可能打印出多次按下 S2 和多次松开。这就是传说中的,有抖动现象。相信大家在写单片机的按键程序时,也必将会涉及一点,就去按键去抖动。按键去抖动的方法无非有二种,一种是硬件电路去抖动,这种在要求不是特别高的情况下是不会被采用的;另一种就是延时去抖动了。而延时又一般分为二种,一种是 for 循环死等待,一种是定时延时,我就是要使用内核的定时器实现去抖动。
猜想四:

......此去省略一万个猜想,你自己想吧。O(∩_∩)O


6.2  高级字符设备驱动之 POLL 机制

这一节里,我们在 irq_key.c 的基础上添加 poll 机制来实现有数据的时候就去读,没数据的时候,自己规定一个时间,如果还没有数据,就表示超时时间。关于什么是 poll 机制,有兴趣的读者,可以参考《Linux 设备驱动第三版》第六章的第三小节。这些都是概念性的知识,Webee 就不抛砖引玉了。不懂的童子们,自觉点哟。


6.2.1  添加 poll 机制的按键驱动实例
添加 poll 机制的中断按键驱动源码在:webee210_drivers\ 5th_advanced_char_drivers\1th_poll\ poll_key.c
与 irq_key.c 相比,只是添加了 poll 函数。


static struct file_operations irq_key_fops = {
.owner
= THIS_MODULE,
.open
= irq_key_open,
.read
= irq_key_read,
.release = irq_key_close,
.poll
= irq_key_poll,
/* 新添加的 */
};


当应用程序调用 poll 函数时,就会最终调用到 irq_key_poll 函数,主要做了二件事。
第一、使用 poll_wait 函数将进程挂在 button_waitq 队列上,而不是立即休眠。
第二、当有数据产生时(有按键动作时),将 poll 的状态记录下来,否则返回 0。


static unsigned irq_key_poll(struct file *file, poll_table *wait)
{
unsigned int mask = 0;
/* 该函数,只是将进程挂在 button_waitq 队列上,而不是立即休眠 */
poll_wait(file, &button_waitq, wait);
/* 当没有按键按下时,即不会进入按键中断处理函数,此时 ev_press = 0
* 当按键按下时,就会进入按键中断处理函数,此时 ev_press 被设置为 1
*/
if (ev_press)
mask |= POLLIN | POLLRDNORM; /* POLLIN 表示有数据可读 */
/* 如果有按键按下时,mask |= POLLIN | POLLRDNORM,否则 mask = 0 */
return mask;
}


6.2.2  添加 poll 机制的按键驱动测试程序
添加 poll 机制的中断按键驱动的测试程序源码在:webee210_drivers\ 5th_advanced_char_drivers\1th_poll\ poll_test.c


/* 某些头文件 */
int main(void)
{
int buttons_fd;

char key_val;
char key_num;
struct pollfd fds;
int ret;
/* 打开设备 */
buttons_fd = open("/dev/IRQ_KEY", O_RDWR);
if (buttons_fd < 0)
{
printf("Can not open device key\n");
return -1;
}
printf(" Please press buttons on webe210 board\n");
fds.fd = buttons_fd;
fds.events = POLLIN;
while(1)
{
/* poll 函数返回 0 时,表示 5s 时间到了,
* 而这段时间里,没有事件发生"数据可读"
*/
ret = poll(&fds,1,5000);
if(ret == 0)
{
printf("time out\n");
}
else
{
/* 读取按键驱动发出的数据,注意 key_value
* 和按键驱动中定义为一致的类型
*/
read(buttons_fd, &key_val, 1);
key_num = key_val;
if(key_num & 0x80)
{
key_num = key_num - 0x80;
printf("S%d is release on!
key_val = 0x%x\n",key_num ,key_val);
}
else
{
printf("S%d is pressed down!
key_val = 0x%x\n", key_num,key_val);

}
}
}
close(buttons_fd);
return 0;
}
测试程序首先打开/dev/IRQ_KEY 设备文件,这个设备文件正是刚才的按键驱动程序创建的,即相当于找到按键硬件资源。然后调用 poll 函数,其中设置超时时间为 5s。它表示,如果 5s 时间到了,而这段时间里,没有事件发生"数据可读",那么就表示超时。换句话说,应用程序不会一直等待事件发生,而是只等 5 秒,如果这 5 秒都还没有事件发生,就返回,表示已经超时。


6.2.3  添加 poll 机制的按键驱动实例测试结果与现象


附录源码:

< driver / poll_key.c >
/*
 * Name:poll_key.c
 * Copyright (C) 2014 Webee.JY  (2483053468@qq.com)
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 */
#include <linux/device.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/irq.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <linux/gpio.h>
#include <linux/sched.h>
#include <linux/poll.h>

#define DEVICE_NAME "IRQ_KEY"

struct button_irq_desc {
    int irq;		/* 中断号 */
    int pin;		/* GPIO引脚 */
    int key_val;	/* 按键初始值 */
    char *name;		/* 名字 */
};

static struct button_irq_desc button_irqs [] = {
	{IRQ_EINT(16), S5PV210_GPH2(0), 0x01, "S1"}, /* S1 */
	{IRQ_EINT(17), S5PV210_GPH2(1), 0x02, "S2"}, /* S2 */
	{IRQ_EINT(18), S5PV210_GPH2(2), 0x03, "S3"}, /* S3 */
	{IRQ_EINT(19), S5PV210_GPH2(3), 0x04, "S4"}, /* S4 */
	
	{IRQ_EINT(24), S5PV210_GPH3(0), 0x05, "S5"}, /* S5 */
	{IRQ_EINT(25), S5PV210_GPH3(1), 0x06, "S6"}, /* S6 */
	{IRQ_EINT(26), S5PV210_GPH3(2), 0x07, "S7"}, /* S7 */
	{IRQ_EINT(27), S5PV210_GPH3(3), 0x08, "S8"}, /* S8 */
};

static DECLARE_WAIT_QUEUE_HEAD(button_waitq);
static struct class *irq_key_class;

/* 键值: 按下时, 0x01, 0x02, 0x03, 0x04,0x05,0x06,0x07,0x08 */  
/* 键值: 松开时, 0x81, 0x82, 0x83, 0x84,0x85,0x86,0x87,0x88 */  
static unsigned char key_val;  

/* 中断事件标志, 中断服务程序将它置1,irq_key_read将它清0 */
static volatile int ev_press = 0;

/* 中断处理函数 */
static irqreturn_t key_interrupt(int irq, void *dev_id)
{
	struct button_irq_desc *button_irqs = (struct button_irq_desc *)dev_id;
	unsigned int pinval;

	pinval = gpio_get_value(button_irqs->pin);
	if (pinval)	
	{		
		/* 松开 */	
		key_val = 0x80 | button_irqs->key_val;	
	}	
	else	
	{		
		/* 按下 */		
		key_val = button_irqs->key_val;
	}
	
	ev_press = 1;

	 /* 唤醒休眠的进程 */
	wake_up_interruptible(&button_waitq);
	
	return IRQ_RETVAL(IRQ_HANDLED);
}

static int irq_key_open(struct inode *inode, struct file *file)
{
	int i;
	int err = 0;
	
	/* 使用request_irq函数注册中断 */
	for (i = 0; i < sizeof(button_irqs)/sizeof(button_irqs[0]); i++)
	{
		err = request_irq(button_irqs[i].irq, key_interrupt, IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, 
                          button_irqs[i].name, (void *)&button_irqs[i]);
	}
	/* 注册中断失败处理 */
	if (err)
	{
		i--;
		for (; i >= 0; i--)
		{
			disable_irq(button_irqs[i].irq);
			free_irq(button_irqs[i].irq, (void *)&button_irqs[i]);
		}
		return -EBUSY;
	}
	return 0;
}

static ssize_t irq_key_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
	if (count != 1)		
		return -EINVAL;

	/* 如果没有按键动作, 休眠,即不会马上执行copy_to_user
	 * ev_press = 0时,进程会休眠,当有按键动作时,
	 * 会进入按键中断处理函数,里面将ev_press = 1,
	 * 然后唤醒进程,然后马上执行copy_to_user,继续往下跑。
	 */
	wait_event_interruptible(button_waitq, ev_press);
	
	/* 如果有按键动作, 返回键值给应用程序 */
	copy_to_user(buf, &key_val, 1);
	ev_press = 0;
	
	return 1;
}

static int irq_key_close(struct inode *inode, struct file *file)
{
	int i;

	/* 注销中断 */
	for (i = 0; i < sizeof(button_irqs)/sizeof(button_irqs[0]); i++)
	{
		free_irq(button_irqs[i].irq, (void *)&button_irqs[i]);
	}

	return 0;
}

static unsigned irq_key_poll(struct file *file, poll_table *wait)
{
	unsigned int mask = 0;

	/* 该函数,只是将进程挂在button_waitq队列上,而不是立即休眠 */ 
	poll_wait(file, &button_waitq, wait); 

	/* 当没有按键按下时,即不会进入按键中断处理函数,此时ev_press = 0  
     * 当按键按下时,就会进入按键中断处理函数,此时ev_press被设置为1 
     */  
	if (ev_press)
		mask |= POLLIN | POLLRDNORM;	/* POLLIN表示有数据可读 */

	/* 如果有按键按下时,mask |= POLLIN | POLLRDNORM,否则mask = 0 */ 
	return mask;
}

static struct file_operations irq_key_fops = {
    .owner   =  THIS_MODULE,    
    .open    =  irq_key_open,     
	.read	 =	irq_key_read,	   
	.release =  irq_key_close,	 
	.poll    =  irq_key_poll,
};

int major;
static int __init Irq_key_init(void)
{
	/* 注册一个字符设备 */
	major = register_chrdev(0, "key_drv", &irq_key_fops);

	/* 成功创建类后,可在/sys/class/目录下找到key_drv类 */
	irq_key_class = class_create(THIS_MODULE, "key_drv");

	/* 在key_drv类下创建/dev/IRQ_KEY 设备,供应用程序打开设备*/
	device_create(irq_key_class, NULL, MKDEV(major, 0), NULL, DEVICE_NAME); 

	return 0;
}

static void Irq_key_exit(void)
{
	unregister_chrdev(major, "key_drv");
	device_destroy(irq_key_class, MKDEV(major, 0));
	class_destroy(irq_key_class);
}


module_init(Irq_key_init);

module_exit(Irq_key_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("webee");
MODULE_DESCRIPTION("Character drivers for irq key");

< driver / Makefile >
ifneq ($(KERNELRELEASE),)
	obj-m :=poll_key.o
else
	module-objs :=poll_key.o
	KERNELDIR :=/home/gec/linux_kernel/linux2.6.35.7/
	PWD :=$(shell pwd)
default:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif

clean:
	$(RM)  *.ko *.mod.c *.mod.o *.o *.order *.symvers *.cmd

< app / poll_test.c >
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <poll.h>

/* 键值: 按下时, 0x01, 0x02, 0x03, 0x04,0x05,0x06,0x07,0x08 */  
/* 键值: 松开时, 0x81, 0x82, 0x83, 0x84,0x85,0x86,0x87,0x88 */  
int main(void)
{
	int buttons_fd;
	char key_val;
	char key_num;
	struct pollfd fds; 
	int ret;

	/* 打开设备 */
	buttons_fd = open("/dev/IRQ_KEY", O_RDWR);
	if (buttons_fd < 0)
	{
		printf("Can not open device key\n");
		return -1;
	}
	printf(" Please press buttons on webe210 board\n");
	
	fds.fd = buttons_fd;  
	fds.events = POLLIN; 
	while(1)
	{
		/* poll函数返回0时,表示5s时间到了,而这段时间里,没有事件发生"数据可读" */  
        ret = poll(&fds,1,5000);  
        if(ret == 0)  
        {  
            printf("time out\n");  
        } 
		else
		{
			/*读取按键驱动发出的数据,注意key_value和按键驱动中定义为一致的类型*/
			read(buttons_fd, &key_val, 1);
			
			key_num = key_val;
			if(key_num & 0x80) 	
			{
				key_num = key_num - 0x80; 
				printf("S%d is release on!     key_val = 0x%x\n",key_num ,key_val);
			}
			else
			{
				printf("S%d is pressed down!   key_val = 0x%x\n", key_num,key_val);
			}	
		  }
	}
	close(buttons_fd);
	return 0;
}

< app / Makefile >
#
#  General Makefile

Exec := poll_test
Obj := poll_test.c
CC := arm-linux-gcc

$(Exec) : $(Obj)
	$(CC) -o $@ $(Obj) $(LDLIBS$(LDLIBS-$(@)))

clean:
	rm -vf $(Exec) *.elf *.o

6.3  高级字符设备驱动之异步通知
在上一节里,我们在中断的基础上添加 poll 机制来实现有数据的时候就去读,没数据的时候,自己规定一个时间,如果还没有数据,就表示超时时间。在此以前,我们都是让应用程序主动去读,那有没有一种情况,当驱动程序有数据时,主动去告诉应用程序,告诉它,有数据了,你赶紧来读吧。答案当然是有的,这种情况在 linux 里的专业术语就叫异步通知。关于什么是异步通知机制,有兴趣的读者,可以参考《Linux 设备驱动第三版》第六章的第四小节。想要在驱动里实现异步通知的效果,有哪些要素?有六个要素:
第一、应用程序要实现有:注册信号处理函数,使用 signal 函数。
第二、谁来发?驱动来发
第三、发给谁?发给应用程序,但应用程序必须告诉驱动 PID(什么是 PID,不会忘了吧?这么快就还给 Webee 啦?)
第四、怎么发?驱动程序使用 kill_fasync 函数来发。
第五、应该在驱动的哪里调用 kill_fasync 函数?kill_fasync 函数的作用是,当有数据时去通知应用程序,理所当然的应该在中断处理函数里调用。
第六、file_operations 需要添加什么函数指针成员吗?要的,需要添加 fasync函数指针,要实现这个函数指针,幸运的是,这个函数仅仅调用了fasync_helper 函数,而且这个函数是内核帮我们实现好了,驱动工程师不用修改,fasync_helper 函数的作用是初始化/释放 fasync_struct 结构体。


6.3.1  添加异步通知的按键驱动实例
添 加 异 步 通 知 机 制 的 中 断 按 键 驱 动 源 码 在 : webee210_drivers\5th_advanced_char_drivers\2th_fasync\ fasync_key.c
在这一节里,我们将在上一节的基础上修改驱动,将其修改为有异步通知功能的按键驱动,目标:按下按键时,驱动主动去通知应用程序。


static struct file_operations irq_key_fops = {
.owner = THIS_MODULE,
.open
= irq_key_open,
.read
= irq_key_read,
.release = irq_key_close,
.poll
= irq_key_poll,
.fasync = irq_key_fasync,
/* 新添加的 */
};
当应用程序调用了 fcntl(fd, F_SETFL, Oflags | FASYNC)时,就会最终调用到 irq_key_fasync 函数。当有按键动作发生时,就会进入中断处理函数,在里面调用 kill_fasync 函数告诉应用程序,有数据可读了 。

static irqreturn_t key_interrupt(int irq, void *dev_id)
{
struct button_irq_desc *button_irqs =
(struct button_irq_desc *)dev_id;
unsigned int pinval;
pinval = gpio_get_value(button_irqs->pin);
if (pinval)

{
/* 松开 */
key_val = 0x80 | button_irqs->key_val;
}
else
{
/* 按下 */
key_val = button_irqs->key_val;
}
ev_press = 1;
/* 唤醒休眠的进程 */
wake_up_interruptible(&button_waitq);
/* 用 kill_fasync 函数告诉应用程序,有数据可读了
* button_fasync 结构体里包含了发给谁(PID 指定)
* SIGIO : 表示要发送的信号类型
* POLL_IN: 表示发送的原因(有数据可读了)
*/
kill_fasync(&button_fasync, SIGIO, POLL_IN);
return IRQ_RETVAL(IRQ_HANDLED);
}


6.3.2  添加异步通知的按键驱动测试程序
添 加 poll 机制的中断按键驱动的测试程序源码在: webee210_drivers\5th_advanced_char_drivers\ 2th_fasync\ fasync_test.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h> //sleep
#include <poll.h>
#include <signal.h>
#include <fcntl.h>
int buttons_fd;
void mysignal_fun(int signum)
{
char key_num,key_val;

/* 读取按键驱动发出的数据,注意 key_val
* 和按键驱动中定义为一致的类型
*/
read(buttons_fd, &key_val, 1);
key_num = key_val;
if(key_num & 0x80)
{
key_num = key_num - 0x80;
printf("S%d is release on!
key_val = 0x%x\n",key_num ,key_val);
}
else
{
printf("S%d is pressed down!
key_val = 0x%x\n", key_num,key_val);
}
}
/* fasync_test
*/
int main(int argc ,char *argv[])
{
int flag;
signal(SIGIO,mysignal_fun);
buttons_fd = open("/dev/IRQ_KEY",O_RDWR);
if (buttons_fd < 0)
{
printf("open error\n");
}
/* F_SETOWN: Set the process ID
* 告诉内核,发给谁
*/
fcntl(buttons_fd, F_SETOWN, getpid());
/* F_GETFL :Read the file status flags
* 读出当前文件的状态
*/
flag = fcntl(buttons_fd,F_GETFL);
/* F_SETFL: Set the file status flags to the value specified by arg

* int fcntl(int fd, int cmd, long arg);
* 修改当前文件的状态,添加异步通知功能
*/
fcntl(buttons_fd,F_SETFL,flag | FASYNC);
while(1)
{
/* 为了测试,主函数里,什么也不做 */
sleep(1000);
}
return 0;
}
测试程序首先调用 signal(SIGIO,mysignal_fun),如果应用程序接收到信号类型为 SIGIO 的信号,就会执行 mysignal_fun 函数,这个函数就是去读取按键值,然后打印按键值。然后打开/dev/IRQ_KEY 设备文件,这个设备文件正是刚才的按键驱动程序创建的,即相当于找到按键硬件资源。最后通过 fcntl 函数的设置,让/dev/IRQ_KEY 设备文件具体异步通知功能。这样第一步的 signal 函数才能够接收信号。
总的来说,当应用程序设置好异步通知功能后,假设有按键动作发生,即有数据产生,就会进入驱动的中断处理函数,调用 kill_fasync(&button_fasync,SIGIO, POLL_IN);告诉应用程序,有数据可以读了。应用程序通过 signal 函数,接收到有 SIGIO 信号,就会调用 mysignal_fun 函数,而这个函数就是去读取按键值。然后,又会进入到驱动程序的 irq_key_read,它通过copy_to_user 将数据拷贝回给应用程序,最终将按键值打印出来。


6.3.3  添加异步通知的按键驱动实例测试结果与现象


附录源码:

< driver / fasync_key.c >
/*
 * Name:fasync_key.c
 * Copyright (C) 2014 Webee.JY  (2483053468@qq.com)
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 */
#include <linux/device.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/irq.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <linux/gpio.h>
#include <linux/sched.h>
#include <linux/poll.h>
#include <linux/fcntl.h> 

#define DEVICE_NAME "IRQ_KEY"

struct button_irq_desc {
    int irq;		/* 中断号 */
    int pin;		/* GPIO引脚 */
    int key_val;	/* 按键初始值 */
    char *name;		/* 名字 */
};

static struct button_irq_desc button_irqs [] = {
	{IRQ_EINT(16), S5PV210_GPH2(0), 0x01, "S1"}, /* S1 */
	{IRQ_EINT(17), S5PV210_GPH2(1), 0x02, "S2"}, /* S2 */
	{IRQ_EINT(18), S5PV210_GPH2(2), 0x03, "S3"}, /* S3 */
	{IRQ_EINT(19), S5PV210_GPH2(3), 0x04, "S4"}, /* S4 */
	
	{IRQ_EINT(24), S5PV210_GPH3(0), 0x05, "S5"}, /* S5 */
	{IRQ_EINT(25), S5PV210_GPH3(1), 0x06, "S6"}, /* S6 */
	{IRQ_EINT(26), S5PV210_GPH3(2), 0x07, "S7"}, /* S7 */
	{IRQ_EINT(27), S5PV210_GPH3(3), 0x08, "S8"}, /* S8 */
};

static DECLARE_WAIT_QUEUE_HEAD(button_waitq);
static struct class *irq_key_class;

/* 键值: 按下时, 0x01, 0x02, 0x03, 0x04,0x05,0x06,0x07,0x08 */  
/* 键值: 松开时, 0x81, 0x82, 0x83, 0x84,0x85,0x86,0x87,0x88 */  
static unsigned char key_val;  

/* 中断事件标志, 中断服务程序将它置1,irq_key_read将它清0 */
static volatile int ev_press = 0;

static struct fasync_struct *button_fasync;

/* 中断处理函数 */
static irqreturn_t key_interrupt(int irq, void *dev_id)
{
	struct button_irq_desc *button_irqs = (struct button_irq_desc *)dev_id;
	unsigned int pinval;

	pinval = gpio_get_value(button_irqs->pin);
	if (pinval)	
	{		
		/* 松开 */	
		key_val = 0x80 | button_irqs->key_val;	
	}	
	else	
	{		
		/* 按下 */		
		key_val = button_irqs->key_val;
	}
	
	ev_press = 1;

	 /* 唤醒休眠的进程 */
	wake_up_interruptible(&button_waitq);

	/* 用kill_fasync函数告诉应用程序,有数据可读了  
     * button_fasync结构体里包含了发给谁(PID指定) 
     * SIGIO  : 表示要发送的信号类型 
     * POLL_IN: 表示发送的原因(有数据可读了) 
     */  
    kill_fasync(&button_fasync, SIGIO, POLL_IN);
	
	return IRQ_RETVAL(IRQ_HANDLED);
}

static int irq_key_open(struct inode *inode, struct file *file)
{
	int i;
	int err = 0;
	
	/* 使用request_irq函数注册中断 */
	for (i = 0; i < sizeof(button_irqs)/sizeof(button_irqs[0]); i++)
	{
		err = request_irq(button_irqs[i].irq, key_interrupt, IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, 
                          button_irqs[i].name, (void *)&button_irqs[i]);
	}
	/* 注册中断失败处理 */
	if (err)
	{
		i--;
		for (; i >= 0; i--)
		{
			disable_irq(button_irqs[i].irq);
			free_irq(button_irqs[i].irq, (void *)&button_irqs[i]);
		}
		return -EBUSY;
	}
	return 0;
}

static ssize_t irq_key_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
	if (count != 1)		
		return -EINVAL;

	/* 如果没有按键动作, 休眠,即不会马上执行copy_to_user
	 * ev_press = 0时,进程会休眠,当有按键动作时,
	 * 会进入按键中断处理函数,里面将ev_press = 1,
	 * 然后唤醒进程,然后马上执行copy_to_user,继续往下跑。
	 */
	wait_event_interruptible(button_waitq, ev_press);
	
	/* 如果有按键动作, 返回键值给应用程序 */
	copy_to_user(buf, &key_val, 1);
	ev_press = 0;
	
	return 1;
}

static int irq_key_close(struct inode *inode, struct file *file)
{
	int i;

	/* 注销中断 */
	for (i = 0; i < sizeof(button_irqs)/sizeof(button_irqs[0]); i++)
	{
		free_irq(button_irqs[i].irq, (void *)&button_irqs[i]);
	}

	return 0;
}

static unsigned irq_key_poll(struct file *file, poll_table *wait)
{
	unsigned int mask = 0;

	/* 该函数,只是将进程挂在button_waitq队列上,而不是立即休眠 */ 
	poll_wait(file, &button_waitq, wait); 

	/* 当没有按键按下时,即不会进入按键中断处理函数,此时ev_press = 0  
     * 当按键按下时,就会进入按键中断处理函数,此时ev_press被设置为1 
     */  
	if (ev_press)
		mask |= POLLIN | POLLRDNORM;	/* POLLIN表示有数据可读 */

	/* 如果有按键按下时,mask |= POLLIN | POLLRDNORM,否则mask = 0 */ 
	return mask;
}

/* 当应用程序调用了fcntl(fd, F_SETFL, Oflags | FASYNC);  
 * 则最终会调用驱动的fasync函数,在这里则是irq_key_fasync 
 * irq_key_fasync最终又会调用到驱动的fasync_helper函数 
 * fasync_helper函数的作用是初始化/释放fasync_struct结构体 
 */  
static int irq_key_fasync(int fd, struct file *filp, int on)  
{  
    return fasync_helper(fd, filp, on, &button_fasync);  
}  

static struct file_operations irq_key_fops = {
    .owner   =  THIS_MODULE,    
    .open    =  irq_key_open,     
	.read	 =	irq_key_read,	   
	.release =  irq_key_close,	 
	.poll    =  irq_key_poll,
	.fasync  = 	irq_key_fasync, 
};

int major;
static int __init Irq_key_init(void)
{
	/* 注册一个字符设备 */
	major = register_chrdev(0, "key_drv", &irq_key_fops);

	/* 成功创建类后,可在/sys/class/目录下找到key_drv类 */
	irq_key_class = class_create(THIS_MODULE, "key_drv");

	/* 在key_drv类下创建/dev/IRQ_KEY 设备,供应用程序打开设备*/
	device_create(irq_key_class, NULL, MKDEV(major, 0), NULL, DEVICE_NAME); 

	return 0;
}

static void Irq_key_exit(void)
{
	unregister_chrdev(major, "key_drv");
	device_destroy(irq_key_class, MKDEV(major, 0));
	class_destroy(irq_key_class);
}


module_init(Irq_key_init);

module_exit(Irq_key_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("webee");
MODULE_DESCRIPTION("Character drivers for irq key");

< driver / Makefile >
ifneq ($(KERNELRELEASE),)
	obj-m :=fasync_key.o
else
	module-objs :=fasync_key.o
	KERNELDIR :=/home/gec/linux_kernel/linux2.6.35.7/
	PWD :=$(shell pwd)
default:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif

clean:
	$(RM)  *.ko *.mod.c *.mod.o *.o *.order *.symvers *.cmd

< app / fasync_test.c >
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>	//sleep
#include <poll.h>
#include <signal.h>
#include <fcntl.h>

int buttons_fd;

void mysignal_fun(int signum)
{
	char key_num,key_val;

	/*读取按键驱动发出的数据,注意key_val和按键驱动中定义为一致的类型*/
	read(buttons_fd, &key_val, 1);
	
	key_num = key_val;
	if(key_num & 0x80) 	
	{
		key_num = key_num - 0x80; 
		printf("S%d is release on!     key_val = 0x%x\n",key_num ,key_val);
	}
	else
	{
		printf("S%d is pressed down!   key_val = 0x%x\n", key_num,key_val);
	}	
}


/* fasync_test
 */ 
int main(int argc ,char *argv[])
{
	int flag;
	signal(SIGIO,mysignal_fun);

	buttons_fd = open("/dev/IRQ_KEY",O_RDWR);
	if (buttons_fd < 0)
	{
		printf("open error\n");
	}

	/* F_SETOWN:  Set the process ID
	 * 告诉内核,发给谁
	 */
	fcntl(buttons_fd, F_SETOWN, getpid());

	/*  F_GETFL :Read the file status flags
	 *  读出当前文件的状态
	 */
	flag = fcntl(buttons_fd,F_GETFL);

	/* F_SETFL: Set the file status flags to the value specified by arg
	 * int fcntl(int fd, int cmd, long arg);
	 * 修改当前文件的状态,添加异步通知功能
	 */
	fcntl(buttons_fd,F_SETFL,flag | FASYNC);
	
	while(1)
	{
		/* 为了测试,主函数里,什么也不做 */
		sleep(1000);
	}
	return 0;
}

< app /Makefile > 
#
#  General Makefile

Exec := fasync_test
Obj := fasync_test.c
CC := arm-linux-gcc

$(Exec) : $(Obj)
	$(CC) -o $@ $(Obj) $(LDLIBS$(LDLIBS-$(@)))

clean:
	rm -vf $(Exec) *.elf *.o

6.4  高级字符设备驱动之定时器去抖动
前面的驱动测试实验,我们说过,有 BUG。什么 BUG 呢?就是按键并不是以按下/松开成对出现的。换句话说,当你只按下/松开一次 S2 时,可能打印出多次按下 S2 和多次松开。这就是传说中的,有抖动现象。相信大家在写单片机的按键程序时,也必将会涉及一点,就去按键去抖动。按键去抖动的方法无非有二种,一种是硬件电路去抖动,这种在要求不是特别高的情况下是不会被采用的;另一种就是延时去抖动了。而延时又一般分为二种,一种是 for 循环死等待,一种是定时延时,我就是要使用内核的定时器实现去抖动。想要使用 Linux 内核定时器,需要设置哪些要素?
第一、设置超时时间
第二、设置处理函数
Linux 内核里的定时器结构是怎样的?


struct timer_list {
struct list_head entry;

unsigned long expires;
void (*function)(unsigned long);
unsigned long data;
struct tvec_base *base;
.....
};
问题一:void (*function)(unsigned long data)里面的参数是谁传给它的?
答:是 timer_list.data 传给它的,如果需要向 function 传递参数时,则应该
设置 timer_list.data,否则可以不设置。
问题二:与定时器相关的操作函数有哪些?
(1) 使用 init_timer 函数初始化定时器
(2) 设置 timer_list.function,并实现这个函数指针
(3) 使用 add_timer 函数向内核注册一个定时器
(4) 使用 mod_timer 修改定时器时间,并启动定时器
问题三:int mod_timer(struct timer_list *timer, unsigned long expires)的第二个参数为超时时间,怎么设置超时时间,如果定时为 10ms?
答:一般的形式为: jiffies + (HZ /100),HZ 表示 100 个 jiffies,jiffies为 10ms,即 HZ = 100*10ms = 1s


6.4.1  添加定时器的按键驱动实例

添 加 异 步 通 知 机 制 的 中 断 按 键 驱 动 源 码 在 : webee210_drivers\5th_advanced_char_drivers\3th_timer\timer_key.c
在这一节里,我们将在上一节的基础上修改驱动,将其修改为有定时器的按键驱动,以达到软件去抖动的目的。在驱动入口函数定义定时器......


/* 定义一个定时器结构体 */
static struct timer_list buttons_timer;
int major;
static int __init Irq_key_init(void)
{
/* 初始化定时器 */
init_timer(&buttons_timer);
/* 当定时时间到达时 buttons_timer_function 就会被调用 */
buttons_timer.function = buttons_timer_function;
/* 向内核注册一个定时器 */
add_timer(&buttons_timer);

/* 注册一个字符设备 */
major = register_chrdev(0, "key_drv", &irq_key_fops);
/* 成功创建类后,可在/sys/class/目录下找到 key_drv 类 */
irq_key_class = class_create(THIS_MODULE, "key_drv");
/* 在 key_drv 类下创建/dev/IRQ_KEY 设备,供应用程序打开设备*/
device_create(irq_key_class, NULL,
MKDEV(major, 0), NULL, DEVICE_NAME);
return 0;
}


在中断处理函数里,使用 mod_timer 修改定时器时间,并启动定时器。
static irqreturn_t key_interrupt(int irq, void *dev_id)
{
irq_pd = (struct button_irq_desc *)dev_id;
/* 修改定时器定时时间,定时 10ms,即 10 秒后启动定时器
* HZ 表示 100 个 jiffies,jiffies 的单位为 10ms,即 HZ = 100*10ms = 1s
* 这里 HZ/100 即定时 10ms
*/
mod_timer(&buttons_timer, jiffies + (HZ /100));
return IRQ_RETVAL(IRQ_HANDLED);
}


当定时时间到达时,buttons_timer_function 就会被调用,这个函数的工作,其实就是以前的中断处理函数的工作,只不过现在由它来承担了。


/* 定时器处理函数 */
static void buttons_timer_function(unsigned long data)
{
struct button_irq_desc *button_irqs = irq_pd;
unsigned int pinval;
pinval = gpio_get_value(button_irqs->pin);
if (pinval)
{
/* 松开 */
key_val = 0x80 | button_irqs->key_val;
}
else
{
/* 按下 */

key_val = button_irqs->key_val;
}
ev_press = 1;
/* 唤醒休眠的进程 */
wake_up_interruptible(&button_waitq);
/* 用 kill_fasync 函数告诉应用程序,有数据可读了
* button_fasync 结构体里包含了发给谁(PID 指定)
* SIGIO : 表示要发送的信号类型
* POLL_IN: 表示发送的原因(有数据可读了)
*/
kill_fasync(&button_fasync, SIGIO, POLL_IN);
}


6.4.2  添加定时器的按键驱动测试程序
添 加 定 时 器 的 中 断 按 键 驱 动 的 测 试 程 序 源 码 在 : webee210_drivers\5th_advanced_char_drivers\3th_timer\timer_test.c


#include <stdio.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/time.h>
#include <errno.h>
/* 键值: 按下时, 0x01, 0x02, 0x03, 0x04,0x05,0x06,0x07,0x08 */
/* 键值: 松开时, 0x81, 0x82, 0x83, 0x84,0x85,0x86,0x87,0x88 */
int main(void)
{
int buttons_fd;
char key_val;
char key_num;
/* 打开设备 */
buttons_fd = open("/dev/IRQ_KEY", O_RDWR);
if (buttons_fd < 0)
{
printf("Can not open device key\n");

return -1;
}
printf(" Please press buttons on webe210 board\n");
while(1)
{
/* 读取按键驱动发出的数据,注意 key_value
* 和按键驱动中定义为一致的类型
*/
read(buttons_fd, &key_val, 1);
key_num = key_val;
if(key_num & 0x80)
{
key_num = key_num - 0x80;
printf("S%d is release on!
key_val = 0x%x\n",key_num ,key_val);
}
else
{
printf("S%d is pressed down!
key_val = 0x%x\n", key_num,key_val);
}
}
close(buttons_fd);
return 0;
}

这个测试程序和 5.3.3 小节的测试程序是一模一样的,这里就不重复讲解了。但是测试结果就不一样了,因为驱动添加了定时器延时去抖动。

定时器延时去抖动的原理就是,假设在定时 10ms 内,没有下降沿/上升沿中断 发 生 ( 相 当 于 没 有 抖 动 发 生 ) , 10ms 定 时 时 间 一 到 , 就 会 调 用buttons_timer_function 定时器处理函数,这个函数就是设置好按键值,唤醒进程,好让按键值返回给应用程序。但是假如 10ms 内,有下降沿/上升沿中断发生(相当于有抖动发生),就会进入中断处理函数,通过 mod_timer 函数重新修改定时器定时时间,这样一来,新的一个 10ms 又产生了。这样 10ms 定时时间没有到,就不会调用 buttons_timer_function 定时器处理函数,这样就不会唤醒进程,从而实现了定时器延时去抖动的目的。


6.3.4  添加定时器的按键驱动实例测试结果与现象


附录源码:

< driver / timer_key.c >
/*
 * Name:timer_key.c
 * Copyright (C) 2014 Webee.JY  (2483053468@qq.com)
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 */
#include <linux/device.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/irq.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <linux/gpio.h>
#include <linux/sched.h>
#include <linux/poll.h>
#include <linux/fcntl.h> 

#define DEVICE_NAME "IRQ_KEY"

struct button_irq_desc {
    int irq;		/* 中断号 */
    int pin;		/* GPIO引脚 */
    int key_val;	/* 按键初始值 */
    char *name;		/* 名字 */
};

static struct button_irq_desc button_irqs [] = {
	{IRQ_EINT(16), S5PV210_GPH2(0), 0x01, "S1"}, /* S1 */
	{IRQ_EINT(17), S5PV210_GPH2(1), 0x02, "S2"}, /* S2 */
	{IRQ_EINT(18), S5PV210_GPH2(2), 0x03, "S3"}, /* S3 */
	{IRQ_EINT(19), S5PV210_GPH2(3), 0x04, "S4"}, /* S4 */
	
	{IRQ_EINT(24), S5PV210_GPH3(0), 0x05, "S5"}, /* S5 */
	{IRQ_EINT(25), S5PV210_GPH3(1), 0x06, "S6"}, /* S6 */
	{IRQ_EINT(26), S5PV210_GPH3(2), 0x07, "S7"}, /* S7 */
	{IRQ_EINT(27), S5PV210_GPH3(3), 0x08, "S8"}, /* S8 */
};

static struct button_irq_desc *irq_pd;

static DECLARE_WAIT_QUEUE_HEAD(button_waitq);
static struct class *irq_key_class;

/* 键值: 按下时, 0x01, 0x02, 0x03, 0x04,0x05,0x06,0x07,0x08 */  
/* 键值: 松开时, 0x81, 0x82, 0x83, 0x84,0x85,0x86,0x87,0x88 */  
static unsigned char key_val;  

/* 中断事件标志, 中断服务程序将它置1,irq_key_read将它清0 */
static volatile int ev_press = 0;

static struct fasync_struct *button_fasync;
static struct timer_list buttons_timer;  /* 定义一个定时器结构体 */ 

/* 中断处理函数 */
static irqreturn_t key_interrupt(int irq, void *dev_id)
{
	irq_pd = (struct button_irq_desc *)dev_id;
	
	/* 修改定时器定时时间,定时10ms,即10秒后启动定时器 
     * HZ 表示100个jiffies,jiffies的单位为10ms,即HZ = 100*10ms = 1s 
     * 这里HZ/100即定时10ms 
     */  
 	mod_timer(&buttons_timer, jiffies + (HZ /100));  
  
	return IRQ_RETVAL(IRQ_HANDLED);
}

static int irq_key_open(struct inode *inode, struct file *file)
{
	int i;
	int err = 0;
	
	/* 使用request_irq函数注册中断 */
	for (i = 0; i < sizeof(button_irqs)/sizeof(button_irqs[0]); i++)
	{
		err = request_irq(button_irqs[i].irq, key_interrupt, IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, 
                          button_irqs[i].name, (void *)&button_irqs[i]);
	}
	/* 注册中断失败处理 */
	if (err)
	{
		i--;
		for (; i >= 0; i--)
		{
			disable_irq(button_irqs[i].irq);
			free_irq(button_irqs[i].irq, (void *)&button_irqs[i]);
		}
		return -EBUSY;
	}
	return 0;
}

static ssize_t irq_key_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
	if (count != 1)		
		return -EINVAL;

	/* 如果没有按键动作, 休眠,即不会马上执行copy_to_user
	 * ev_press = 0时,进程会休眠,当有按键动作时,
	 * 会进入按键中断处理函数,里面将ev_press = 1,
	 * 然后唤醒进程,然后马上执行copy_to_user,继续往下跑。
	 */
	wait_event_interruptible(button_waitq, ev_press);
	
	/* 如果有按键动作, 返回键值给应用程序 */
	copy_to_user(buf, &key_val, 1);
	ev_press = 0;
	
	return 1;
}

static int irq_key_close(struct inode *inode, struct file *file)
{
	int i;

	/* 注销中断 */
	for (i = 0; i < sizeof(button_irqs)/sizeof(button_irqs[0]); i++)
	{
		free_irq(button_irqs[i].irq, (void *)&button_irqs[i]);
	}

	return 0;
}

static unsigned irq_key_poll(struct file *file, poll_table *wait)
{
	unsigned int mask = 0;

	/* 该函数,只是将进程挂在button_waitq队列上,而不是立即休眠 */ 
	poll_wait(file, &button_waitq, wait); 

	/* 当没有按键按下时,即不会进入按键中断处理函数,此时ev_press = 0  
     * 当按键按下时,就会进入按键中断处理函数,此时ev_press被设置为1 
     */  
	if (ev_press)
		mask |= POLLIN | POLLRDNORM;	/* POLLIN表示有数据可读 */

	/* 如果有按键按下时,mask |= POLLIN | POLLRDNORM,否则mask = 0 */ 
	return mask;
}

/* 当应用程序调用了fcntl(fd, F_SETFL, Oflags | FASYNC);  
 * 则最终会调用驱动的fasync函数,在这里则是irq_key_fasync 
 * irq_key_fasync最终又会调用到驱动的fasync_helper函数 
 * fasync_helper函数的作用是初始化/释放fasync_struct结构体 
 */  
static int irq_key_fasync(int fd, struct file *filp, int on)  
{  
    return fasync_helper(fd, filp, on, &button_fasync);  
}  

static struct file_operations irq_key_fops = {
    .owner   =  THIS_MODULE,    
    .open    =  irq_key_open,     
	.read	 =	irq_key_read,	   
	.release =  irq_key_close,	 
	.poll    =  irq_key_poll,
	.fasync  = 	irq_key_fasync, 
};

/* 定时器中断处理函数 */  
static void buttons_timer_function(unsigned long data)  
{  
	struct button_irq_desc *button_irqs = irq_pd;
	unsigned int pinval;

	pinval = gpio_get_value(button_irqs->pin);
	if (pinval)	
	{		
		/* 松开 */	
		key_val = 0x80 | button_irqs->key_val;	
	}	
	else	
	{		
		/* 按下 */		
		key_val = button_irqs->key_val;
	}
	
	ev_press = 1;

	 /* 唤醒休眠的进程 */
	wake_up_interruptible(&button_waitq);

	/* 用kill_fasync函数告诉应用程序,有数据可读了  
     * button_fasync结构体里包含了发给谁(PID指定) 
     * SIGIO  : 表示要发送的信号类型 
     * POLL_IN: 表示发送的原因(有数据可读了) 
     */  
    kill_fasync(&button_fasync, SIGIO, POLL_IN);
} 

int major;
static int __init Irq_key_init(void)
{
	/* 初始化定时器 */  
    init_timer(&buttons_timer);  
    /* 当定时时间到达时buttons_timer_function就会被调用 */  
    buttons_timer.function  = buttons_timer_function;  
    /* 向内核注册一个定时器 */  
    add_timer(&buttons_timer);

	/* 注册一个字符设备 */
	major = register_chrdev(0, "key_drv", &irq_key_fops);

	/* 成功创建类后,可在/sys/class/目录下找到key_drv类 */
	irq_key_class = class_create(THIS_MODULE, "key_drv");

	/* 在key_drv类下创建/dev/IRQ_KEY 设备,供应用程序打开设备*/
	device_create(irq_key_class, NULL, MKDEV(major, 0), NULL, DEVICE_NAME); 

	return 0;
}

static void Irq_key_exit(void)
{
	unregister_chrdev(major, "key_drv");
	device_destroy(irq_key_class, MKDEV(major, 0));
	class_destroy(irq_key_class);
	del_timer(&buttons_timer);
}


module_init(Irq_key_init);

module_exit(Irq_key_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("webee");
MODULE_DESCRIPTION("Character drivers for irq key");

< driver / Makefile >
ifneq ($(KERNELRELEASE),)
	obj-m :=timer_key.o
else
	module-objs :=timer_key.o
	KERNELDIR :=/home/gec/linux_kernel/linux2.6.35.7/
	PWD :=$(shell pwd)
default:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif

clean:
	$(RM)  *.ko *.mod.c *.mod.o *.o *.order *.symvers *.cmd

< app / timer_test.c >
#include <stdio.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/time.h>
#include <errno.h>


/* 键值: 按下时, 0x01, 0x02, 0x03, 0x04,0x05,0x06,0x07,0x08 */  
/* 键值: 松开时, 0x81, 0x82, 0x83, 0x84,0x85,0x86,0x87,0x88 */  
int main(void)
{
	int buttons_fd;
	char key_val;
	char key_num;

	/* 打开设备 */
	buttons_fd = open("/dev/IRQ_KEY", O_RDWR);
	if (buttons_fd < 0)
	{
		printf("Can not open device key\n");
		return -1;
	}
	printf(" Please press buttons on webe210 board\n");

	while(1)
	{
		/*读取按键驱动发出的数据,注意key_value和按键驱动中定义为一致的类型*/
		read(buttons_fd, &key_val, 1);
		
		key_num = key_val;
		if(key_num & 0x80) 	
		{
			key_num = key_num - 0x80; 
			printf("S%d is release on!     key_val = 0x%x\n",key_num ,key_val);
		}
		else
		{
			printf("S%d is pressed down!   key_val = 0x%x\n", key_num,key_val);
		}		
	}
	close(buttons_fd);
	return 0;
}

< app / Makefile >
#
#  General Makefile

Exec := timer_test
Obj := timer_test.c
CC := arm-linux-gcc

$(Exec) : $(Obj)
	$(CC) -o $@ $(Obj) $(LDLIBS$(LDLIBS-$(@)))

clean:
	rm -vf $(Exec) *.elf *.o

6.5  本章小结
终于到了本章小结部分了,是不是感觉本章学习的内容太多了?来总结一下本章学习了什么呢?回忆 3 秒钟......本章讲解了 POLL 机制,它实现了应用程序可以不用一直死读设备文件,而是设定一定的时间,如果在这段时间内没有数据可以读取的话,就表示超时,而不会继续往下读。本章还讲解了异步通知机制,既然前面一直都是应用程序主动去读驱动里面的数据,那么可不可以等驱动自己有数据的时候再去告诉应用程序,你来读我吧。可以的,6.3 小节的异步通知机制实现的正是这样一个功能。
本章最后讲解了定时器延时去抖动的实例,通过这个实例,让大家对 Linux内核定时器有个初步认识,最关键的还是要掌握如何在驱动里添加定时器,灵活运用定时器。通过这个实例,就成功解决了前面一直说的 BUG、BUG、BUG,妈妈再也不用担心按下/松开不成对出现的 BUG 了。
高级字符驱动的内容,还远远不止这些,限于篇幅,这里就不再介绍更多的内容了。提示一下,有关原子、自旋锁、信号量、读写锁这些同步互斥机制,请自行参考《Linux 设备驱动第三版》第五章并发和竞争相关知识。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值