文件储存在硬盘上,硬盘的最小存储单位叫做”扇区”(Sector)。每个扇区储存512字节(相当于0.5KB)。
操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读
取一个”块”(block)。这种由多个扇区组成的”块”,是文件存取的最小单位。”块”的大小,最常见的是4KB,
即连续八个 sector组成一个 block。文件数据都储存在”块”中,那么很显然,我们还必须找到一个地方储存
文件的元信息,比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做
inode,中文译名为”索引节点”
索引节点对象inode结构体,定义文件在linux/fs.h中
/*
* Keep mostly read-only and often accessed (especially for
* the RCU path lookup and 'stat' data) fields at the beginning
* of the 'struct inode'
*/
struct inode {
umode_t i_mode; //唯一的标识与一个设备文件关联,内核在i_mode中存储量文件类型
unsigned short i_opflags;
kuid_t i_uid; //inode拥有者id
kgid_t i_gid; //inode所属群组id
unsigned int i_flags;
#ifdef CONFIG_FS_POSIX_ACL
struct posix_acl *i_acl;
struct posix_acl *i_default_acl;
#endif
const struct inode_operations *i_op; // 默认的索引节点操作
struct super_block *i_sb; //相关的超级块
struct address_space *i_mapping; //相关的地址映射
#ifdef CONFIG_SECURITY
void *i_security;
#endif
/* Stat data, not accessed from path walking */
unsigned long i_ino;
/*
* Filesystems may only read i_nlink directly. They shall use the
* following functions for modification:
*
* (set|clear|inc|drop)_nlink
* inode_(inc|dec)_link_count
*/
union {
const unsigned int i_nlink;
unsigned int __i_nlink;
};
dev_t i_rdev; //表示设备文件的结点,这个域实际上包含了设备号
loff_t i_size; //inode所代表大少
struct timespec i_atime; //inode最近一次的存取时间
struct timespec i_mtime; //inode最近一次修改时间
struct timespec i_ctime; //inode的生成时间
spinlock_t i_lock; /* i_blocks, i_bytes, maybe i_size */
unsigned short i_bytes;
unsigned int i_blkbits;
blkcnt_t i_blocks;
#ifdef __NEED_I_SIZE_ORDERED
seqcount_t i_size_seqcount;
#endif
/* Misc */
unsigned long i_state;
struct rw_semaphore i_rwsem;
unsigned long dirtied_when; /* jiffies of first dirtying */
unsigned long dirtied_time_when;
struct hlist_node i_hash;
struct list_head i_io_list; /* backing dev IO list */
#ifdef CONFIG_CGROUP_WRITEBACK
struct bdi_writeback *i_wb; /* the associated cgroup wb */
/* foreign inode detection, see wbc_detach_inode() */
int i_wb_frn_winner;
u16 i_wb_frn_avg_time;
u16 i_wb_frn_history;
#endif
struct list_head i_lru; /* inode LRU list */
struct list_head i_sb_list;
union {
struct hlist_head i_dentry;
struct rcu_head i_rcu;
};
u64 i_version;
atomic_t i_count;
atomic_t i_dio_count;
atomic_t i_writecount;
#ifdef CONFIG_IMA
atomic_t i_readcount; /* struct files open RO */
#endif
const struct file_operations *i_fop; /* former ->i_op->default_file_ops */
struct file_lock_context *i_flctx;
struct address_space i_data;
struct list_head i_devices;
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev; //若是字符设备,对应的为cdev结构体
char *i_link;
unsigned i_dir_seq;
};
__u32 i_generation;
#ifdef CONFIG_FSNOTIFY
__u32 i_fsnotify_mask; /* all events this inode cares about */
struct hlist_head i_fsnotify_marks;
#endif
#if IS_ENABLED(CONFIG_FS_ENCRYPTION)
struct fscrypt_info *i_crypt_info;
#endif
void *i_private; /* fs or device private pointer */
};
struct file {
/*
* fu_list becomes invalid after file_free is called and queued via
* fu_rcuhead for RCU freeing
*/
union {
struct list_head fu_list;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
#define f_dentry f_path.dentry
#define f_vfsmnt f_path.mnt
const struct file_operations *f_op;
spinlock_t f_lock; /* f_ep_links, f_flags, no IRQ */
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
loff_t f_pos;
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;
u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data;
#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_head f_ep_links;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
#ifdef CONFIG_DEBUG_WRITECOUNT
unsigned long f_mnt_write_state;
#endif
}
struct file
f_path里存储的是open传入的路径,VFS就是根据这个路径逐层找到相应的inode
f_inode里存储的是找到的inode
f_op里存储的就是驱动提供的file_operations对象,这个对象在open的时候被填充,具体地,应用层的open通过层层搜索会调用inode.i_fops->open,即chrdev_open()
f_count的作用是记录对文件对象的引用计数,也即当前有多少个使用CLONE_FILES标志克隆的进程在使用该文件。典型的应用是在POSIX线程中。就像在内核中普通的引用计数模块一样,最后一个进程调用put_files_struct()来释放文件描述符。
f_flags当打开文件时指定的标志,对应系统调用open的int flags,比如驱动程序为了支持非阻塞型操作需要检查这个标志是否有O_NONBLOCK。
f_mode;对文件的读写模式,对应系统调用open的mod_t mode参数,比如O_RDWR。如果驱动程序需要这个值,可以直接读取这个字段。
private_data表示file结构的私有数据
struct cdev {
struct kobject kobj; //内嵌的内核对象.
struct module *owner; //该字符设备所在的内核模块的对象指针
const struct file_operations *ops;//该结构描述了字符设备所能实现的方法,是极为关键的一个结构体
struct list_head list; //用来将已经向内核注册的所有字符设备形成链表
dev_t dev; //字符设备的设备号,由主设备号和次设备号构成.
unsigned int count; //隶属于同一主设备号的次设备号的个数.
};
struct miscdevice {
int minor; //次设备号
const char *name; //设备名字
const struct file_operations *fops; //接口函数
struct list_head list;
struct device *parent;
struct device *this_device;
const char *nodename;
mode_t mode;
};
struct device {
/* The linked-list pointer. */
struct device *next;
/* The device's descriptor, as mapped into the Guest. */
struct lguest_device_desc *desc;
/* We can't trust desc values once Guest has booted: we use these. */
unsigned int feature_len;
unsigned int num_vq;
/* The name of this device, for --verbose. */
const char *name;
/* Any queues attached to this device */
struct virtqueue *vq;
/* Is it operational */
bool running;
/* Does Guest want an intrrupt on empty? */
bool irq_on_empty;
/* Device-specific data. */
void *priv;
}
struct work_struct {
atomic_long_t data;
#define WORK_STRUCT_PENDING 0 /* T if work item pending execution */
#define WORK_STRUCT_STATIC 1 /* static initializer (debugobjects) */
#define WORK_STRUCT_FLAG_MASK (3UL)
#define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK)
struct list_head entry;
work_func_t func;
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
字符设备文件自动创建只需要三个保证和四个函数
1、保证根文件系统rootfs脚本文件中添加以下两句话:
(1)/bin/mount -a:就是为了执行fstab文件(位于etc目录)
(2)echo /sbin/mdev > /proc/sys/kernel/hotplug说明:表面看是向文件hotplug写入字符串“/sbin/mdev”,本 质是将来驱动要创建设备文件时,驱动会自动解析hotplug文件,找到/sbin/mdev,并且执行此命令mdev, 让mdev命令来帮助驱动创建设备文件。mdev为创建驱动的工具。
2、保证根文件系统rootfs必须有mdev命令
在下位机执行which is mdev查看是否存在即可
3、保证根文件系统rootfs必要配置文件fstab中必须有以下两句话:
#device mount-point type options dump fsck order
proc /proc proc defaults 0 0
sysfs /sys sysfs defaults 0 0
结论:
将proc虚拟文件系统挂接到/proc目录下
即:mount -t proc proc /proc
将sysfs虚拟文件系统挂接到/sys目录下
即:mount -t sysfs sysfs /sys
类似:U盘挂接
挂接U盘:mount -t vfat /dev/sda1 /mnt
注意/proc、/sys目录里面的内容由内核自己创建,创建于内存中,掉电丢失。
四个函数:给mdev提供原材料
struct class *cls //创建一个设备类指针
//创建一个设备类对象,名称为hezeu
//THIS_MODULE是内核常量,固定不变
cls指向创建的设备类对象
1、cls=class_create(THIS_MODULE,"hezeu");
//正式创建设备文件,本质是此函数将来会调用mdev来帮助它创建设备文件,此函数给mdev提供原材料
//dev:设备号
//myled:设备文件名
2、device_create(cls,NULL,dev,NULL,"myled");
//删除设备文件
3、device_destroy(cls,dev);
//删除设备类对象
4、class_destroy(cls);
混杂设备驱动编程框架
混杂设备驱动定义
混杂设备驱动的主设备号由Linux内核已经定义好为10.各个混杂设备通过次设备号区分
混杂设备驱动的数据结构
struct miscdevice
{
int minor;
const char *name;
const struct file_operations *fops;
struct list_head list;
struct device *parent
struct device *this_device;
const char *nodename;
mode_t mode;
};
minor:混杂设备的次设备号,主设备号内核定义为10
一般指定为宏:MISC_DYNAMIC_MINOR,也就是让内核来帮你分配一个空闲的次设备号
name:混杂设备名,关键是设备文件无需调用四个函数,内核帮你调用四个函数来创建设备文件。但是,三个保证还得自己完成。
fops:给混杂设备驱动添加的硬件操作接口
配套函数:
/向内核注册一个混杂设备
misc_register(注册的混杂设备对象地址)
//从内核卸载混杂设备
misc_deregister(注册的混杂设备对象地址);
注意:
1、设备号由内核帮你分配 现在无需调用alloc_chrdev_region
2、设备文件由内核帮你创建 现在无需调用四个函数
ps//查看pid
top//查看进程btn_test占用的CPU资源比例
Q退出top命令
内核中断
为什么引入中断?因为外设的数据处理速度远远慢于cpu的数据处理速度,所以引入中断。
以CPU读取uart接收缓冲区的数据为例。
1、当CPU读取UART接收缓冲区的数据时,发现数据没有准被就绪(因为接收移位器按照波特率为115200的速度从RX接收数据的速度比CPU核以指针的形式从接收缓冲区数据的速度要慢)
2、首先会想到——以轮询的方式来判断接收缓冲区是否准备好数据(while(!UTRSTATE&0X01));
轮询方式就是CPU核什么事都不能去做,只能原地等,而且是一个忙等待的过程,在等待期间,大量的宝贵CPU资源被白白的浪费掉,并且功耗提高,大大降低CPU的利用率
3、对于这种情况,务必想到轮询的对头——中断机制来解决这个问题,采用中断处理的流程如下:
当CPU读取UART接收缓冲器的数据时,发现数据没有准被就绪,此时CPU就去干其他的事情,就不会while(1)忙等,将来UART接收缓冲区一旦数据准备就绪,UART控制器就会给CPU发送一个中断电信号,一旦CPU接收到了中断电信号,立马停止当前的事情转去从UART接收缓冲区中将数据读走,读取完毕CPU核再返回到原先的事情继续运行。大大提高了CPU的利用率。
中断硬件连接和触发流程:如上图
1、UART控制器一旦准备好数据,UART控制器通过和中断控制器之间的中断信号线给中断控制器发送一个中断电信号。
2、一旦中断控制器接收到UART控制器发送的中断电信号,中断控制器开启各种判断
3、首先判断UART控制器和中断控制器间的中断信号线的中断功能是否使能了,如果被禁止了,中断控制器直接丢弃中断电信号,如果使能了,中断控制器继续判断。
4、然后判断发送来的中断电信号是否是有效的中断触发电信号,中断触发的有效电信号由五种:
(1)高电平触发
(2)低电平触发
(3)下降沿触发
(4)上升沿触发
(5)双边沿触发
如为有效信号,中断控制器继续判断
5、中断控制器判断当前CPU核是否正在执行或处理一个高优先级的中断信号,如果有高优先级的中断,中断控制器直接丢弃,如果没有,中断控制器继续判断
6、然后中断控制器判断将来到底给哪个CPU核发送中断信号,这里只给CPU0和发送
7、最后中断控制器判断到底以哪种形式给CPU核发送中断信号,以IRQ发?以FIQ发?以IRQ发,即通过 IRQ给CPU核发中断信号
8、CPU一旦接到中断信号,CPU核立马触发一个IRQ中断异常,CPU核立马开启异常的处理流程
9、CPU核立马硬件上先做四件事:
(1)备份CPSR到SPSR_IRQ
(2)设置CPSR的BIT[7:0]=110XXXX
(3)保存返回地址LR_IRQ=PC-4
(4)设置PC=0X18
至此开启软件处理IRQ中断异常流程
10、软件处理IRQ中断异常流程,四件事
(1)提前建立异常向量表
(2)保护现场,保护被打断进程的ARM寄存器的数据到栈中,此过程又称压栈
(3)根据用户需求调用IRQ中断处理函数 例如:CPU从UART接收缓冲区读取的数据
(4)恢复现场,状态恢复,跳转返回
CPU核又回到原先被打断的进程继续运行,至此UART触发的IRQ中断处理完毕。
中断的软件编程步骤
1、编写异常向量表的代码,利用汇编和链接器完成
2、编写保护现场的代码
3、根据用户的需求编写中断处理函数
4、编写恢复现场,状态恢复,跳转返回的代码。
注意:在实际的开发时,不管是ARM裸板开发还是基于Linux系统的中断编程,程序员只需要完成第三步即可,也就是只需要编写一个中断服务函数即可,第1,2,4三步骤要不由芯片厂家写好,要不由Linux内核写好
Linux内核驱动编写中断处理函数的两个函数
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long falgs,
const char *name
void *dev)
功能:在Linux内核中,处理器的每一个硬件中断资源都是一个宝贵的资源,如果驱动程序想要访问某个硬件中断
资源,必须向内核申请这个资源,一旦申请成功,然后向内核内核注册这个硬件中断的中断处理函数,一旦注册成
功,中断处理函数就等着硬件中断的触发,一旦硬件中断触发,内核最终调用注册的中断处理函数。总结:此函数
完成两个功能:
1、申请硬件中断资源
2、注册硬件中断的中断处理函数
参数:irq:在Linux内核中,Linux内核给每个硬件中断都分配一个软件编号,简称中断号,类似硬件中断的身份
证号,所以驱动要想申请某个硬件中断资源,irq参数只需传递这个硬件中断的中断号即可
例如:
硬件GPIO GPIO编号 中断号
GPIOC0(3) GPIO_GPC0(3) gpio_to_irq(GPIO_GPC0(3))
handler:本质就是一个函数指针类型
typedef irqreturn_t (*irq_handler_t)(int,void *);
将来只需要传递一个函数即可,此函数就是要注册的中断处理函数,只需要传递一个中断处理函数名(函数地址)即可
中断处理函数原型
irqretrun_t ABC(int irq,void *dev)
{
//根据用户需求完成业务逻辑
return IRQ_NONE;//中断处理函数执行失败
或
return IRQ_HANDLED;//中断处理函数执行成功
或
return IRQ_WAKE_THREAD;//中断处理函数执行成功返回以后,捎带着唤醒一个休眠进程
}
中断处理函数ABC参数说明:
irq:保存当前触发的硬件中断的中断号
例如:假设如果四个按键共用一个中断处理函数,也就是只要有一个按键中断触发,都会调用中断处理
函数,此时irq就保存对应的触发硬件中断的中断号
dev:保存申请中断时给中断处理函数传递的参数
所以第二个参数handler传递ABC即可
flags:中断标志
如果对于外部中断(原理图上可以看到中断线,例如按键),只需制定以下宏即可,也就是指定中断的触发方式:
IRQ_TRIGGER_HIGH:高电平触发
IRQ_TRIGGER_LOW:低电平触发
IRQ_TRIGGER_RISING:上升沿触发
IRQ_TRIGGER_FALLING:下降沿触发
IRQ_TRIGGER_RISING|IRQ_TRIGGER_FALLING:双边沿触发
如果是内部中断(控制器触发的中断),直接给0即可 ,中断触发方式通过寄存器指定。
name:给硬件中断指定一个名称,用于调试,每当申请中断完毕,执行cat /proc/interrupts能够通过name查看中断是否申请成功
dev:给中断处理函数ABC传递的参数,将来中断处理函数ABC的第二个参数dev能够 接收传递的参数
类似:线程创建
int data = 250;
pthread_create(&tid,NULL,thread_func,&data)
pthread_func(void *dev)
{
printf("%d\n",*(int *)dev);
}
free_irq(int irq,void *dev);
irq:释放的硬件中断对应的中断号
dev:给中断处理函数传递的参数 此参数务必和request_irq的第五个参数保持一致如果不传参给出NULL
/proc/interrupts
第一列:中断号
第二列:中断触发次数
第三列:中断类型
第四列:中断的名称,即request_irq的第四个参数
Linux内核中断编程之顶半部和底半部机制
Linux内核中任务分为三类
硬件中断:有中断处理函数,外设给CPU核发送的中断信号线产生的中断
软中断:有中断处理函数,程序调用swi指令立马会触发软中断
进程:有执行函数
不管是什么函数都会完成一定的功能
注意:中断包含硬件中断和软件中断
,中断不隶属于任何进程,永远在内核空间运转
任何任务想要运行必须获得CPU资源
进程靠时间片轮转,中断不参与进程调度
优先级:衡量任务抢夺CPU资源的能力
任务优先级越高,获取CPU资源的能力越强,越早的投入运行
三类任务的优先级的划分
硬件中断的优先级高于软中断
软中断的优先级高于进程
进程之间存在高低优先级之分
软中断之间存在两级优先级
在内核中,硬件中断无优先级之分(哪怕中断控制器支持优先级)
休眠:休眠仅用于进程,中断的世界无休眠
进程休眠是指进程要休眠会释放占用的CPU资源。进程调度器会将CPU资源给别的进程
结论:切记:在Linux内核中,内核要求中断处理函数执行速度要快,如果中断处理函数长时间占用CPU资源,将大大降低系统的并发能力和响应能力。
中断处理函数更不能进行休眠操作
但是并不是所有的中断都能满足内核的要求,有些中断处理的业务比较繁杂,势必造成执行时间过长,势必影响并发能力和响应能力
例如:网卡
容易造成丢包。
想要解决此类问题,利用内核提供的中断顶半部和底半部机制来优化
内核顶半部和底半部机制原理
再次明确:将来实际开发,先不考虑顶半部和底半部机制,先中规中矩的写中断处理函数,如果发现中断处理函数对系统性能影响比较大或者出现数据不正常,此时再考虑使用顶半部和底半部机制。
顶半部特点
1、顶半部本质还是中断处理函数,只是现在要做原先中断处理函数中比较紧急,耗时较短的内容
2、只要硬件中断触发,立马执行顶半部
3、CPU在执行顶半部的中断处理函数期间,不允许发送CPU资源的切换【优先级已经最高了】
4、同样不能进行休眠操作
底半部的特点
1、底半部执行原来中断处理函数中不紧急,耗时较长的内容
2、底半部同样需要一个处理函数,此函数中完成不紧急,耗时较长的内容,此函数又称延后处理函数
3、CPU在执行底半部的延后处理函数期间,如果又有高优先级的任务,是可以抢夺CPU资源,也就是允许CPU资源发生切换
4、底半部本质就是延后执行的一种手段,底半部机制不一定非要和顶半部配对使用,如果仅仅是单纯延后执行,是可以没有顶半部的,只需单独使用底半部即可
5、底半部实现方式三种:
tasklet
工作队列
软中断
底半部机制之tasklet
1、tasklet特点
1.1、tasklet基于软中断实现,所以其优先级高于进程低于硬件中断
1.2、由于基于软中断实现,所以期延后处理函数做不紧急,耗时较长的内容,但是不能进行休眠操作
1.3、CPU在执行其延后处理函数期间,高优先级的软中断或者硬件中断可以抢夺CPU资源
总结一句话:基于软中断实现,不能休眠
2、Linux内核描述tasklet的数据结构
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long data);//
unsigned long data;//
}
功能:描述tasklet数据结构
参数:
func:指向的函数就是tasklet的延后处理函数,此函数执行时的优先级是低于硬件中断高于进程,
基于软中断实现,所以其不能执行休眠操作,形参data:保存给延后处理函数传递的参数
data:给延后处理函数传递的参数
配套函数:
tasklet_schedule / tasklet_init / tasklet_kill
3、利用tasklet实现延后执行的编程步骤:
3.1 明确:tasklet不一定非要和顶半部配合使用,可以单独使用
3.2 定义初始化tasklet对象
int g_data = 250;
struct tasklet_struct ABC; //定义对象
tasklet_init(&ABC, //对象
abc_tasklet_function, //延后处理函数
(unsigned long)&g_data); //传递的参数
3.3 根据用户需求编写延后处理函数
void abc_tasklet_function(unsigned long data)
{
//g_data保存传递的参数,例如:data=(unisgned long)&g_data;
//使用时注意数据类型的强制转换
//此函数不能进行休眠操作
}
3.4 向内核登记注册tasklet对象和其延后处理函数,一旦登记完毕,CPU在“适当”的时候执行tasklet的延后处理函数,登记函数如下:
tasklet_schdeule(&ABC);
问:在哪里登记?
答:根据用户需求在驱动代码的任何地方都可以进行登记,只要登记完成,就等着调用,但是如果有顶半部了,一般在顶半部的中断处理函数中进行登记
2.6底半部机制之工作队列
1、工作队列特点
1.1 工作队列诞生的本质目的就是解决tasklet的延后处理函数不能休眠的问题,因为有些时候需要在延后处理的内容中进行休眠操作,此时不能再采用tasklet,选择工作队列
1.2 工作队列基于进程实现,所以工作队列的延后处理函数是可以休眠的,优先级低于中断
1.3工作队列也是延后执行的一种手段,不一定非和顶半部配对使用
总结:工作队列基于进程实现,可以进行休眠操作
2、Linux内核描述工作队列的数据结构
struct work_struct
{
void (*function)(struct work_struct *work);
....
}
function:指定的延后处理函数,是可以进行休眠操作的
形参:work指针指向驱动自己定义初始化的工作对象,切记work形参要配合内核宏:container_of完成传递参数的功能
3、利用工作队列实现延后执行的编程方法:
3.1定义初始化工作对象
struct work_struct ABC;//定义对象
INIT_WORK(&ABC,//对象
abc_work_function)//延后处理函数
3.2 根据用户需求编写延后处理函数
void abc_work_function(struct work_struct *work)
{
//不紧急,耗时较长的内容
//可以休眠
//work=&ABC;//指向自己,配合container_of
}
3.3 根据业务逻辑,在驱动代码中相应的位置注册登记,一旦登记完成,内核在适当的时候调用工作队列的延后处理函数
schedule_work(&ABC);
如果有顶半部,一般在顶半部进行登记
2.6 底半部机制之软中断【了解即可】
2.7总结
tasklet和工作队列的区别
如果考虑延后处理执行,并且延后处理中有休眠,只能用工作队列
如果考虑延后处理执行,并且考虑效率,并且无休眠,采用tasklet
如果考虑延后处理执行,并且不考虑效率,并且无休眠,采用工作队列
Linux内核时间相关概念
2.1硬件定时器的特点
1、硬件定时器会周期性的按照一定的频率给CPU核发送中断信号,频率软件上可配置,现在很多处理器内部都集成多路的定时器硬件(Timer)
2、硬件定时器在Linux内核中的中断处理函数由芯片厂家完成,也就是硬件定时器的中断处理函数会周期性的被内核调用,内核的定时器中断处理函数一般做以下工作:
1、更新系统的运行时间
2、更新实际时间(又称wall-time)
3、检查当前进程的时间片是否用尽,如果用尽进程调度器决定调度
4、检查内核中已经注册的软件定时器,是否有超时的,如果有超时,内核调用其超时处理函数(类似alarm)
5、统计系统的资源,如:top
2.2 HZ
它是Linux内核一个全局变量
X86架构:HZ=1000;
ARM架构:HZ=100;
以ARM为例,HZ=100,表示硬件定时器一秒钟给CPU核发送100次定时器中断信号,一次定时器中断的时间间隔为10ms,也就是硬件定时器的中断处理函数1s中被调用100次,每发送一次中断的时间间隔为10ms。
2、jiffies_64
内核全局变量,数据类型为unsigned long long(64位),它用来记录自开机以来硬件定时器发送的中断次数,每发生一次,硬件定时器中断处理函数就会让jiffies_64加1(单位为硬件定时器中断触发的次数)
由于每次发生一次定时器中断的时间间隔为10ms,所以jiffies_64也可以用于表示系统运行了多长时间:时间=jiffies_64*10ms。
3、jiffies
内核的全局变量,数据类型为unsigned long(32位)。
硬件定时器每发送一次中断,对应的中断处理函数就会让jiffies取jiffies_64的低32位的值,也就是每发生一次定时器中断,jiffies也会加1
它用来记录流失时间(时间间隔)。
注意:只要在代码中看到jiffies,表示当前代码执行到这里对应的当前时间,简称jiffies就是当前时间
例如:unsigned long timeout = jiffies+5*HZ;
说明:jiffies:表示当前时间
5*HZ=5*100=500:跟jiffies相加,说明要加500次定时器中断,每次10ms,所以5*HZ对应的时间为5s
timeout:5s以后那一时刻的时间。
例子:分析以下代码存在的严重漏洞
unsigned long timeout = jiffies+5*HZ
//以下有一堆代码,CPU执行这些代码也需要时间
//此时jiffies会10ms加1次
.....
//执行完以上代码,判断是否出现超时现象
//此刻的jiffies和上一个jiffies肯定不一样
if(jiffies>timeout)
超时
els
没超时
解决
if(time_after(jiffies,timeout))
超时
else
没超时
3.2Linux内核软件定时器的特点
1、特点
1.1 切记:内核软件定时器基于软中断实现,优先级高于进程而低于硬件中断,所以其超时处理函数千万不能进行休眠操作。
1.2 软件定时器能够指定一个超时时间和超时处理函数,超时时间一旦到期,内核就会调用其超时处理函数,并且内核同时删除超时的定时器,所以软件定时器的超时处理函数只执行一次。
2:内核描述软件定时器的数据结构
struct time_list{
unsigned long expires;
void (*function)(unsigned long);
unsigned long data;
...
};
expires:给定时器指定一个超时时候的时间(不是时间间隔)
例如:expires = jiffies+5*HZ;
function:给定时器指定一个超时处理函数,此函数不能进行休眠操作,形参:data:保存传递的参数
data:给超时处理函数传递的参数,类似tasklet
3、内核定时器编程步骤
3.1定义初始化软件定时器对象
struct time_list ABC;//定义对象
init_timer(&ABC);
//额外自己初始化关键的三个字段
unsigned long g_data = 250;
ABC.function = abc_timer_function;//超时处理函数
ABC.expires = jiffies+2*HZ;//指定超时时间
ABC.data = (unsigned long)&g_data;//传递参数
3.2根据用户需求编写超时处理函数
void abc_timer_function(unsigned long data)
{
data = (unsigned long)&g_data;
//不能进行休眠操作
}
3.3向内核添加注册定时器,一旦注册,定时器开始倒计时,一旦时间到期,内核自动调用超时处理函数,并且删除注册的定时器
add_timer(&ABC);
3.4如果定时器未到期,但是想让定时器停止工作,删除定时器
del_timer(&ABC);
3.5修改定时器,重新指定一个新的超时时间
mod_timer(&ABC,jiffies+10*HZ);
修改定时器的超时时间为10s
注意:此函数等价于完成以下三个步骤:
1、先删除未到期的定时器del_timer
2、重新修改超时时间:expires = jiffies+10*HZ;
3、重新向内核添加新的定时器:add_timer
切记:mod_timer内部已经已经帮你完成互斥访问工作
代码中尽量少写:
ABC.expires=....
add_timer(...);
这两条语句很容易被中断
4、Linux内核延时的相关函数
概念:
延时:又称等待
延时分为两类:忙延时和休眠延时
4.2忙延时的特点
1.忙延时:任务如果要进行忙延时,任务将会原地进行打转空转,死等,耗费占用的CPU资源
2.忙延时应用于等待时间极短的场合
3.忙延时可以应用于任何任务(中断和进程)
4.3忙延时的内核函数(s/ms/us/ns)
void ndelay(int n);//纳秒级忙延时
例如:ndelay(100);//忙延时100ns
void udelay(int n);//微秒级忙延时
例如:udelay(100);//忙延时100us
void mdelay(int n);//毫秒级忙延时
例如:mdelay(100);//忙延时100ms
注意:一般如果忙延时超过10ms,建议采用休眠延时
4.4 休眠延时特点
1、休眠延时:只能用于进程,不能用于硬件中断和软件中断,进程需要进行休眠延时时,进程不会原地空转,而是释放占用的CPU资源给其他进程
2、休眠延时函数
void msleep(int msec);//毫秒级休眠延时
例如msleep(500);//休眠延时500ms
注意执行流程:当进程利用系统调用进入内核空间,调用msleep(500),立马进程会释放占用的CPU资源给其他进程使用,此进程此时代码停止不前,进入休眠状态,等待唤醒。
唤醒的方法有两种:
1、休眠的时间500ms到期,内核主动来唤醒休眠的进程
2、通过kill给休眠的进程发送信号,来唤醒休眠的进程,一旦进程被唤醒,进程立马获取CPU资源并且从msleep函数中返回继续往下执行。
void ssleep(int msec);//秒级休眠延时
例如:ssleep(500);//休眠延时500s
schedule()//永久性休眠
注意执行流程:当进程利用系统调用进入内核空间,调用schedule,立马进程会释放占用的CPU资源给其他进程使用,此进程此时代码停止不前,进入休眠状态,等待唤醒。
唤醒的方法只用一种:
1、通过kill给休眠进程发送信号,来唤醒休眠的进程,一旦进程被唤醒,进程立马获取CPU资源并且从schedule函数中返回继续往下执行。
schedule_timeout(5*HZ);//休眠时间的单位为硬件定时器中断触发的次数
例如:schedule_timeout(5);//休眠延时50ms
注意执行流程:当进程利用系统调用进入内核空间,调用schedule_timeout(5),立马进程会释放占用的CPU资源给其他进程使用,此进程此时代码停止不前,进入休眠状态,等待唤醒。
唤醒的方法有两种:
1、休眠的时间50ms到期,内核主动来唤醒休眠的进程
2、通过kill给休眠的进程发送信号,来唤醒休眠的进程,一旦进程被唤醒,进程立马获取CPU资源并且从schedule_timeout函数中返回继续往下执行。
以上函数发现:这些休眠函数可以做到让进程随时随地的休眠,但是做不到让进程随时随地的被唤醒并且正常运行。解决这个问题必须利用内核的等待队列机制来实现(核心内容)
Linux内核并发和竞态(高级部分)
案例:要求led设备只能被一个应用程序打开一次。
思路:有两种解决方案:
1、在应用层面实现,利用进程间通信方式来实现各个进程的询问查询,但是这种实现软件设计极其繁琐
2、在驱动层面实现:不管有多少个进程来访问设备,都需要先进行open,最终都会调用到底层驱动的唯一open函数,只需在底层驱动open函数中做相关的简单代码限定即可
参考代码:
led_drv.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include "linux/gpio.h"
static int open_cnt=1;
struct led_resource
{
char *name;
int gpio;
};
static struct led_resource led_info[]=
{
{
.name = "LED1",
.gpio = S5PV210_GPC0(3)
},
{
.name = "LED2",
.gpio = S5PV210_GPC0(4)
}
};
static int led_open(struct inode *inode,
struct file *file)
{
if(--open_cnt !=0)
{
printk("open myled failed......\n");
open_cnt++;
return -EBUSY;//杩斿洖澶辫触
}
printk("open myled successful......\n");
gpio_set_value(led_info[0].gpio,1);
return 0;
}
static int led_close(struct inode *inode,
struct file *file)
{
open_cnt++;
gpio_set_value(led_info[0].gpio,0);
return 0;
}
static struct file_operations led_fops={
.open=led_open,
.release=led_close
};
static struct miscdevice led_misc={
.minor=MISC_DYNAMIC_MINOR,
.name="myled",
.fops=&led_fops
};
static int led_init(void)
{
int i;
misc_register(&led_misc);
for(i=0;i<ARRAY_SIZE(led_info);i++)
{
gpio_request(led_info[i].gpio,led_info[i].name);
gpio_direction_output(led_info[i].gpio,0);
}
return 0;
}
static void led_exit(void)
{
int i;
misc_deregister(&led_misc);
//杈撳嚭=锛岄噴鏀綠PIO璧勬簮
for(i=0;i<ARRAY_SIZE(led_info);i++)
{
gpio_set_value(led_info[i].gpio,0);
gpio_free(led_info[i].gpio);
}
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
led_test.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(void)
{
int fd;
fd=open("/dev/myled",O_RDWR);
if(fd<0){
printf("open device failed\n");
return -1;
}
sleep(10);
close(fd);
return 0;
}
下位机测试:
insmod led_drv.ko
./led_test//启动A进程打开
./led_test//启动B进程打开,失败
4.2通过分析以上代码,其中存在一个严重的漏洞。
分析:
1、通过分析led_drv.c驱动,发现其中一条语句至关重要:--open_cnt;
2、回顾C对变量如何使用
static int open_cnt = 1;//分配一块内存空间,字节并且存储1,内存对CPU来说就是外设,既然是外设,CPU访问必须采用地址指针的方式访问,内存仅仅存放数据,数据运算最终放在CPU核中完成,运算完的数据结构最终存到内存变量访问操作:先找到这个变量在内存中的地址,然后以地址指针的形式从内存读取数据到CPU核的寄存器中,然后做运算,运算的结果最终再给内存中。
总结:C程序的一个运算表达式在微观看来,需要四个步骤:
(1)先找内存地址
(2)以地址取出数据
(3)数据运算
(4)写回到内存
并发和竞态
相关概念
并发:多个执行单元(包括中断和进程)同时发生
竞态:多个执行单元对共享资源的同时访问形成竞争的状态
形成竞态必须具备三个条件:
1、执行单元的个数要大于2个
2、必须存在共享资源
3、还要同时访问
共享资源:软件上的全局变量或者硬件资源【寄存器等】
临界区:访问共享资源的代码区域
互斥访问:当一个执行单元在访问临界区时,其他执行单元禁止访问临界区,直到前一个执行单元访问完毕才能访问
执行路径具有原子性:当一个任务获取到CPU资源访问临界区时,如果有严格的时间要求,不允许有CPU资源的切换
2、内核中形成竞态的四种情况
2.1多核
2.2同一个CPU核上的进程与进程之间的抢占,优先级不同
2.3中断和进程
硬件中断抢占进程的CPU资源
软件中断抢夺进程的CPU资源
2.4中断和中断
硬件中断抢占软中断的CPU资源
高优先级软中断抢夺低优先级软中断的CPU资源
3、Linux内核中解决静态引起异常的方法
(1)中断屏蔽
(2)自旋锁
(3)信号量
(4)原子操作
3.1解决竞态引起异常之中断屏蔽
1、中断屏蔽的特点
1.1中断屏蔽能够解决以下竞态问题
进程与进程之间的抢占
中断与进程
中断与中断
多核引起的竞态无法解决
1.2中断屏蔽保护的临界区代码执行速度要快,更不能进行休眠操作。因为内核很多机制与中断相关,如硬件、软件定时器、tasklet、进程调度 、系统调用、各种外设,长时间的屏蔽中断会造成系统运行的紊乱,甚至系统崩溃。
2、利用中断屏蔽解决竞态问题的编程步骤
2.1、编写驱动时,先不要考虑竞态问题,先根据用户需求完成基本功能再说,先不写加保护的代码
2.2编写完毕,再回头查看驱动代码中是否存在竞态问题。此时先看驱动代码中是否存在共享资源,
确定哪些是共享资源,至于多个任务、同时访问问题等,不要考虑,只要有共享资源直接加保护。
2.3一旦确定了共享资源,立即再确定临界区,一旦临界区确定完毕,接下来就是保护临界区
2.4还要观察临界区中是否有休眠,如果有休眠,不要考虑中断屏蔽,如果没有休眠,可以考虑。
2.5如果采用中断屏蔽,访问临界区之前先屏蔽中断,方法如下:
unsigned long flags;
local_irq_save(flags);//屏蔽中断,并且保存中断标志到flags,给内核用
2.6任务此时可以踏踏实实的访问临界区,此时不再会有进程抢占,此时不会再有中断抢占
2.7访问临界区之后,记得要恢复中断。
local_irq_restore(flags);//恢复中断
2.8注意:屏蔽中断和恢复中断要在逻辑上配对使用
3.5Linux内核解决竞态引起异常之自旋锁
1、自旋锁的特点
(1)、自旋锁=自旋+锁,锁不会自旋
(2)、自旋锁必须附加在某个共享资源上
(3)、访问临界区之前必须先获取自旋锁,如果获取成功,任务可以访问临界区,如果任务获取自旋锁失败,任务将会原地空转忙等待【任务会自旋】,直到获取自旋锁的任务释放自旋锁,其他任务才能获取自旋锁成功,才能,去访问临界区【类似互斥锁】。
(4)、自旋锁保护的临界区执行速度要快,更不能进行休眠操作
(5)、自旋锁应用于中断和进程
(6)、自旋锁能够解决以下两个竞态问题:
多核SMP和进程之间的抢占,【虽然基于软中断,但是抢占还需一个操作开关,自旋锁会将这个开关给关闭造成抢占失败,即使来软中断也没用】
自旋锁解决不了:中断和进程、中断和中断的竞态问题,一旦中断触发该抢占还得抢占
2、内核描述自旋锁的数据类型spinlock_t
3、利用自旋锁保护临界区的编程步骤
3.1 先不要考虑竞态问题,先根据用户需求编写代码,完善功能
3.2 然后再回头检查驱动代码中是否存在竞态问题,很简单先找共享资源
3.3 然后确定代码中哪些是临界区,将来保护就是临界区,至于哪些任务来打断,来抢占不要考虑,只保护即可
3.4 如果采用自旋锁,然后确定临界区中是否有休眠操作,如果有,势必不考虑自旋锁,如果没有,还要考虑是否有中断参与的竞态问题,
这里的中断不要局限于自己代码中的中断相关的函数(硬件中断处理函、tasklet延后处理函数、软件定时器的超时处理函数),
心里还要明白即使自己的代码中没有这些中断处理函数,内核中还有许多其他中断来抢占(例如:硬件定时器每隔10ms触发一个中断)
结论:如果发现有中断参与,势必不考虑自旋锁,如果没有,可以考虑使用自旋锁
3.5 如果采用自旋锁,首先要定义初始化自旋锁对象
spinlock_t lock;//定义对象
spin_lock_init();/初始化
3.6访问临界区之前先获取自旋锁
spin_lock(&lock);
说明:如果任务获取自旋锁成功,立马从此函数返回继续向下运行访问临界区
如果任务获取自旋锁失败,任务在此函数中进行忙等待
while(1)
{
if(锁被释放了)
break;
}
3.7 此时任务可以踏踏实实的访问临界区
此时没有内核的参与
此时没有进程抢占
但是如果有中断照样抢,只要不访问临界区,抢占没关系
3.8访问临界区完毕,记得释放自旋锁
spin_unlock(&lock);
3.9 注意:最后要认真检查获取锁和释放锁是否在逻辑上配对,否则出现死锁。
分析:虽然采用自旋锁保护临界区也会有操作系统的其他中断来抢夺CPU资源,但是操作系统的其他中断的处理函数访问不到全局共享资源open_cnt,因为有static做了一层保护。
假如将来驱动代码中添加了自己的中断处理函数,并且访问open_cnt,显然自旋锁不能再使用了
3.6Linux内核解决竞态引起异常之衍生自旋锁
1、衍生自旋锁的特点
(1)衍生自旋锁基于自旋锁实现,扩展的自旋锁
(2)衍生自旋锁 = 自旋 + 衍生锁,锁不会自旋
(3)衍生自旋锁必须附加在某个共享资源上
(4)访问临界区之前必须先获取衍生自旋锁,如果获取成功,任务可以访问临界区,如果任务获取衍生自旋锁失败,任务将会原地空转忙等待【任务会自旋】,直到获取衍生自旋锁的任务释放自旋锁,其他任务才能获取衍生自旋锁成功,才能去访问临界区【类似互斥锁】
(5)衍生自旋锁应用于中断和进程
(6)衍生自旋锁能够解决所有的竞态问题:
衍生自旋锁 = 中断屏蔽 + 自旋锁
2、内核描述衍生自旋锁的数据类型spinlock_t
3、利用衍生自旋锁保护临界区的编程步骤
3.1 先不要考虑竞态问题,先根据用户需求编写代码,完善功能
3.2 然后回头检查代码中是否存在竞态问题,很简单先找共享资源
3.3 然后确定代码中哪些是临界区,将来保护就是保护临界区,至于哪些任务来打断,来抢占不要考虑,只保护即可
3.4 如果采用衍生自旋锁,然后确定临界区中是否有休眠操作,如果有,势必不考虑衍生自旋锁,如果没有,可以考虑使用衍生自旋锁
3.5 如果采用衍生自旋锁,首先要定义初始化衍生自旋锁对象
spinlock_t lock;//定义对象
spinlock_Init(&lock);//初始化
3.6访问临界区之前先获取衍生自旋锁
unsigned flags;
spin_lock_irqsave(&lock,falgs);//先关闭中断,然后获取自旋锁
说明:如果任务获取衍生自旋锁成功,立马从此函数返回继续向下运行访问临界区;如果任务获取衍生自旋锁失败,任务在此函数中进行忙等待
while(1)
{
if(锁释放了)
break;
}
3.7 此时任务可以踏踏实实的访问临界区,此时没有任何竞态会发生
3.8 访问临界区完毕,记得释放衍生自旋锁和恢复中断
spin_unlock_irqrestore(&lock,flags);
3.9 注意:最后要认真检查获取锁和释放锁是否在逻辑上配对,否则出现死锁。
3.7Linux内核解决竞态引起异常之信号量
1、信号量的特点
(1)信号是进程间通信的一种方式,信号量是进程同步的方式
(2)内核的信号量和Linux系统编程的信号量一模一样。
(3)中断屏蔽、自旋锁、衍生自旋锁都有一个致命的缺点,保护的临界区不能休眠,有些临界区中需要进行休眠等待操作,此时只能用信号量
(4)信号量只能用于进程
(5)信号量又称睡眠锁
(6)如果进程要访问临界区,首先要获取信号量;获取信号量成功,可以访问临界区;获取信号量失败,进程立马进入休眠等待状态;进程会释放占用的CPU资源,等待被唤醒
2、Linux内核描述信号量的数据结构struct semaphore
3、利用信号量来保护临界区的编程步骤
3.1 先写功能代码,完成用户的基本功能需求
3.2 回头检查竞态问题,先找共享资源,然后确定临界区
3.3 然后观察临界区中是否有休眠,如果有休眠只能用信号量,如果没有休眠,可以考虑信号量【其他方法也可以使用】
3.4 访问临界区前,定义初始化信号量对象为互斥信号量,互斥信号量的值只有1(没有人用)或0(有人获取到了)
struct semaphore sema;//定义信号量对象
sema_init(&sema,1);//初始化信号量的初值为1
3.5 访问临界区之前先获取信号量down(&sema);
说明:
1、功能:获取信号量,本质将信号量sema的值由1减为0,如果进程获取信号量成功,进程立马从此函数返回,继续向下进行访问临界区,
如果进程获取信号量失败,进程在此函数中进入不可中断的休眠状态,直到被唤醒,并且从此函数返回继续向下运行。
2、不可中断的休眠类型:进程在休眠期间,如果接受到了kill信号,此进程不会被立即唤醒,一旦将来被唤醒以后,
进程也不能不去处理接收到了的信号,醒来以后立马去处理之前接收到的信号
3、不可中断的休眠进程唤醒的方法只有一种:持有信号量的任务访问临界区之后来唤醒休眠的进程,简称驱动主动唤醒。
“驱动主动唤醒”=“内核唤醒”,驱动代码调用唤醒函数来唤醒休眠进程
down_interruptible(&sema);
说明:
1、功能:
获取信号量,本质将信号量sema的值由1减为0,如果进程获取信号量成功,进程立马从此函数返回,继续向下进行访问临界区,
如果进程获取信号量失败,进程在此函数中进入可中断的休眠状态,直到被唤醒,并且从此函数返回继续向下运行。
2、可中断的休眠类型:进程在休眠期间,如果接收到了kill信号,此进程会被立即唤醒,醒来以后立马去处理之前接收到的信号
3、可中断的休眠进程唤醒的方法只有两种:
(1)持有信号量的任务访问临界区之后来唤醒休眠的进程,简称驱动主动唤醒。如果是驱动主动唤醒,进程立马获取信号量
可以访问临界区。驱动主动唤醒=内核唤醒,驱动代码调用唤醒函数来唤醒休眠进程
(2)休眠期间接收到了kill信号,立马被唤醒。此时进程不能访问临界区
如何区分哪个唤醒?
if(down_interruptible(&sema))
{
printk("进程接收到了信号量引起唤醒");
}
else
{
printk("驱动主动唤醒");
}
3.6 获取信号量成功,任务访问临界区
3.7 访问完毕,记得要释放信号量
up(&sema);//本质是将信号量的值由0->1
功能:
(1)、唤醒休眠的进程,又称驱动主动唤醒【内核唤醒】
(2)、释放信号量
3.8 检查获取信号量和释放信号量逻辑上要配对
3.8Linux内核解决 竞态引起异常之原子操作
1、原子操作的特点:
原子操作适用于中断和进程
能够解决所有的竞态问题
分两类:位原子操作和整型原子操作
2、位原子操作
2.1 位原子操作==位操作+原子==位操作具有原子性==对共享资源进行位操作时,不允许发生CPU资源的切换
3、Linux内核提供的位原子操作的相关函数
明确:如果将来驱动要对共享资源进行位操作,并且要考虑到竞态问题,此时此刻可以利用位原子操作来解决竞态引起的异常问题,只需调用内核提供的相关操作函数即可完成。
void set_bit(void * addr,int nr)
将addr地址内数据的第nr位(nr从0开始)设置为1
void clear_bit(void *addr,int nr);
将addr地址内数据的第nr位(nr从0开始)清为0.
void change_bit(void *addr,int nr);
将addr地址内数据的第nr位(nr从0开始)反转
void test_bit(void *addr,int nr);
获取addr地址内数据的第nr位(nr从0开始)的值
再次强调:这些函数主要功能是解决对共享资源位操作竞态引起的异常问题,它们只是顺带着做了一个位操作而已
一句话:主业是解决竞态,副业是位操作
参考代码:
int open_cnt = 1;//临界资源
open_cnt &= ~(1<<1);//临界区,目前代码无保护
优化添加保护:
方案1:中断屏蔽
unsigned long flags;
local_irq_save(flags);
open_cnt &= ~(1<<1);
local_irq_restore(flags);
方案2:采用衍生自旋锁
unsigned long flags;
spin_lock_irqsave(&lock,flags);
open_cnt &= ~(1<<1);
spin_unlock_irqrestore(&lock,flags);
方案3:采用信号量
down_interruptible(&sema);
open_cnt &= ~(1<<1);
up(&sema);
方案4:采用位原子操作
clean_bit(&open_cnt,1);
4、整型原子操作
整型原子操作=整型操作+原子=整型操作具有原子性=对共享资源进行整型数的操作期间不允许发生CPU资源的切换
5、整型原子操作编程步骤
1、Linux内核给整形原子操作提供了专有的数据类型:atomic_t,注意:atomic_t本质是一个struct结构体,使用时类比成int类型。
atomic_t tv = ATOMIC_INIT(1);//定义初始化整型原子变量的值为1;
tv = 1;//不允许
2、Linux内核同样给整形原子操作提供了相关的操作函数
atomic_add/atomic_sub/atomic_read/atomic_test....
atomic_add(tv,1);
3、应用场景:如果将来对共享资源进行整型数的操作,并且考虑到竞态问题,可以考虑使用整型原子操作来解决,
但是共享资源的数据结构不再是char/short/int/long...,一律替换成atomic_t(类比成char/short/int/long...)
参考代码:
int open_cnt = 1;//共享资源
open_cnt += 1;//临界区,无保护
优化代码添加保护
方案1:中断屏蔽
unsigned long falgs;
local_irq_save(flags);
open_cnt += 1;
local_irq_restore(flags);
方案2:采用衍生自旋锁
unsigned long flags;
spin_lock_irqsave(&lcok,flags);
open_Cnt+=1;
spin_unlock_irqrestore(&lock,flags);
方案3:采用信号量
down_interruptible(&sema);
pen_cnt+=1;
up(&sema);
方案4:采用整型原子操作
atomic_t open_cnt = ATOMIC_INIT(1);
atomic_init(&open_cnt);//open_cnt++
int atomic_dec_and_test(atomic_t *v)
//自减操作后测试其是否为0
//为0则返回true,否则返回false
4、Linux内核等待队列机制【核心中的核心】
4.1明确 Linux系统经典的三大队列
1、消息队列,进程间通信的方式【七种之一】
2、工作队列,底半部机制实现的一种方式,本质是延后执行
3、等待队列,让进程在内核空间随时随地的休眠,随时随地的唤醒
Linux系统进程休眠的方法
1、用户空间休眠的方法sleep
例如sleep(2000);
2、内核空间休眠
msleep/ssleep/schedule/schedule_timeout
总结:以上休眠函数致命的弱点:虽然能够让进程在用户空间或者内核空间 随时随地的休眠,但是做不到让进程随时随地的唤醒,如果采用以上休眠函数,让进程休眠并且唤醒休眠进程的方法有两种:
(1)、休眠时间到期
(2)、接收kill信号
如何让进程随时随地的休眠,并且随时随地的唤醒呢?
采用等待队列机制
等待队列机制应用场景
此时此刻举例阐述等待队列机制的应用场景:
以CPU读取UART接收缓冲区为例,读取数据的流程:
(1)、首先启动一个应用程序,也就是启动了一个进程,并且要想读取数据,调用read函数: read(fd,buf,1024);
(2)进程由于调用read,最终进程跑到内核UART驱动的read接口函数中读取UART接收缓冲区的数据
(3)由于接收移位器从RX数据线上接收数据的速度(115200bps)要比CPU从UART接收缓冲区读取数据的速度要慢
假设此时UART接收缓冲区数据没有准被就绪
(4)首先想到读进程在底层驱动的read接口函数中采用轮询方式判断数据是否准备就绪
int uart_read(...)
{
while(1)
{
if(数据准备好了)
break;
}
}
但是采用轮询方式相当耗费CPU资源,所以想到采用中断方式,目前先搁一边
但是CPU资源现在被读取进程占用,此时用该不能让进程一直拿着CPU资源,读进程应该释放占用的CPU资源给别的进程,如何释放?很简单让进程区休眠等待;等待UART接收缓冲区的数据准被就绪。
(5)、再次回到中断,如果将来UART接收缓冲区数据准被就绪,UART控制器立马给CPU核发送一个中断电信号,CPU核 开启中断异常处理,最终CPU调用UART的中断处理函数,此时UART中断处理函数从UART接收缓冲区将数据读取 到内核缓冲区暂存起来,然后做一个关键的事情:唤醒之前休眠的读进程,整个休眠过程没有指定休眠时间,休眠可 以很短【说明数据交互频繁】,休眠也可以很长【说明数据没有交互】,但是只要有数据来,就立马唤醒!如果采用 ssleep(10),休眠期间如果数据准被就绪,还在休眠造成数据的丢失。
(6)、要完成进程的休眠和唤醒必须采用等待队列。
结论:等待队列诞生的根本原因:就是外设的数据处理速度远远慢于CPU的数据处理速度,操作数据的进程就需要进行休眠等待操作。有中断必然有等待队列,有等待队列不一定有中断
2.4 等待队列应用场景:以CPU读取按键信息为例
应用->read(按键名 按键值 状态)->驱动read->无按键操作->进程在驱动read休眠,释放CPU资源,CPU做其他事->等待用户按键操作->操作按键->产生中断->内核调用中断处理函数->获取按键信息保存到全局变量->唤醒read进程->read读取保存按键信息的全局数据到用户空间的缓冲区->至此read执行完毕
结论:产生等待队列的根本原因:外设慢
等待队列的作用:让进程在内核空间随时随地的休眠,并且随时随地的被唤醒,用中断必然有等待队列,有等待队列不一定有中断
2.5Linux内核等待队列机制编程步骤,编程方法1:
(进程调度器:负责进程的CPU资源分配配和释放,负责进程之间的调度,抢占等,内核已经实现。休眠等待队列头、各个休眠进程需要完成)
比如老鹰抓小鸡的游戏
1、类比
老鹰<------->进程调度器:负责进程的CPU资源分配和释放,负责进程之间的调度,抢占等
鸡妈妈<----->休眠等待队列头
各个小鸡<------>各个休眠的进程,每个小鸡对应一个休眠的进程
2、定义初始化等待队列头对象(构造武装一个鸡妈妈)
wait_queue_head_t wq;//定义等待队列头
init_waitqueue_head(&wq);//初始化等待队列头
注意:不同的等待队列代表不同等待的事件;等待的事件不一样,队列不一样
3、定义初始化装载休眠进程的容器(小鸡)
注意:每个小鸡,每个容器对应一个休眠的进程,所以容器定义初始化必须是局部的,不能是全局的。
wait_queue_t wait;//定义容器,用来装载休眠的进程
init_waitqueue_entry(&wait,current);//将当前进程添加到容器wait中
当前进程:正在获取CPU资源运行的进程
current:它是内核的全局指针变量,对应的数据类型:
struct task_struct
{
volatile long state;//进程状态
pid_t pid;//进程ID
char comm[TASK_COMM_LEN]//进程名称
};
功能:描述Linux内核属性的数据结构
生命周期:每当启动一个进程,内核就会帮你用此数据结构定义初始化一个对象来描述新奇启动进程的各种属性,每当进程退出,内核同时帮你销毁之前创建的对象
结论:current指针就是指向当前进程对应的task_struct对象,驱动将来通过current可以获取当前进程的各种属性。
如:printk("进程[%s] [%d]\n",current->comm,current->pid);
注意:每个容器对应一个休眠的进程,所以容器的定义初始化必须是局部的,不能是全局的
例如://全局容器
wait_queue_t wait;//全局变量
或者
wait_queue_t wait[3];//全局数组
缺点:无法满足多个进程
//局部:定义在函数内部
例如:
//按键驱动的read接口
ssize_t btn_read(...)
{
wait_queue_t wait;//局部的
}
只要进程调用read,都会访问驱动的read接口,驱动read接口就会为当前进程创建一个容器wait。
4、将当前要休眠的进程添加到等待队列中(将小鸡添加到鸡妈妈带领的队列中)
add_wait_queue(&wq,&wait);
注意:此时此刻进程还没有休眠
5、指定当前进程将来要休眠的类型
可中断的休眠类型
set_current_state(TASK_INTERRUPTIBLE);
或者
不可中断的休眠类型
set_current_state(TASK_UNINTERRUPTIBLE);
注意:此时此刻进程还没有休眠
回顾:
可中断的休眠类型:休眠期间,如果接收到了kill信号会立即被唤醒处理接收到的kill信号,唤醒的方式有两种:
(1)、驱动主动唤醒,驱动调用唤醒函数来唤醒
(2)、接收到kill信号来唤醒
不可中断的休眠类型:休眠期间,如果接收到了kill信号不会立即被唤醒处理接收到的kill信号,直到驱动主动唤醒以后 去处理接收到的信号,唤醒的方式只有一种:
(1)、驱动主动唤醒:驱动调用唤醒函数来唤醒
6、此时此刻进程正式进入休眠等待
此时此刻代码停止不前,此时当前进程释放CPU【进程调度器(老鹰)】,当前进程静静等待被唤醒,如果一旦被唤醒,进程再次获取CPU资源【进程调度器(老鹰)】继续向下运行,休眠方法schedule();
7、一旦进程被唤醒,从schedule()函数返回继续运行,然后设置进程的状态为运行
set_current_state(TASK_RUNNING);
8、然后将唤醒的进程从等待队列中移除
remove_wait_queue(&wq,&wait);
9、如果是可中断休眠类型,最后还要判断哪个原因引起的唤醒;
//判断当前进程之前是否接收到过kill信号
if(signal_pending(current))
{
printk("当前进程由于接收到了kill信号引起的唤醒\n");
return ERESTARTSYS;
}
else
{
printk("事件到来,事件满足,启动主动唤醒休眠的进程");
}
10、事件一旦满足【例如UART数据准被就绪】,驱动主动唤醒休眠的进程,方法如下:
wake_up(&wq);
//唤醒wq等待队列中所有的休眠进程
或者
wake_up_interrrptible(&wq);
//唤醒wq等待队列中可中断的休眠进程
案例:写进程唤醒读进程
案例:基于上一个案例,改造成一个按键驱动
需求:应用程序能够获取按键的键值和按键的状态并打印输出
程序02 执行流程
应用程序第一次执行read->btn_read:休眠
等待按键操作
按键按下->产生中断->中断处理函数调用;
(1)、获取状态和按键值
(2)、唤醒read进程
->read进程继续运行->拷贝状态和键值到用户缓冲区->应用第一次read结束
由于应用while(1),CPU立马又启动第二次read->btn_read;
休眠
此时手松开->产生中断->中断处理函数调用;
(1)获取状态和按键值
(2)唤醒read进程
->read进程继续运行->拷贝状态和键值到用户缓冲区->应用第二次read结束
下位机测试:
2.6Linux内核等待队列机制编程步骤,编程方法2:
1、明确:编程方法2只是高度封装了编程方法1的某些步骤,只需三步即可搞定,具体实施步骤如下:
1、定义初始化等待队列头(构造鸡妈妈)
wait_queue_head_t wq;
init_waitqueue_head(&wq);
2、直接调用以下宏函数,让进程进入休眠等待状态,一旦被唤醒,进程继续运行;
wait_event(wq,condition);
说明:(1)、如果为真,进程立马从此宏函数返回继续运行,不会休眠
(2)、如果为假,进程将进入不可中断的休眠状态,直到被唤醒,一旦被唤醒,进程从此宏函数返回继续运行
注意:此进程的唤醒方法只有一个,驱动主动唤醒或者:
wait_event_interruptible(wq,condition);
说明:(1)、如果为真,进程立马从此宏函数返回继续运行,不会休眠
(2)、如果为假,进程将进入可中断的休眠状态,直到被唤醒,一旦被唤醒,进程从此宏函数返回继续运行。
注意:此进程的唤醒方法有两个,驱动主动唤醒和kill
方法2 的第2步包含了方法1 的3-9步
3、事件一旦被满足,驱动主动唤醒休眠,方法如下:
wake_up(&wq);
//唤醒wq等待队列中所有的休眠进程
wake_up_interruptible(&wq);
//唤醒wq等待队列中可中断的休眠进程
4、方法2 的编程框架
//休眠的代码地方
...xxxx(...)
{
//进入休眠
wait_event/wait_event_interruptible
condition = 假;为下一次休眠做好准备
}
唤醒的代码地方
...yyy(...)
{
condition = 真;
wake_up/wake_uP_interruptible
}
注意:唤醒之前置假为返回
唤醒以后置真为休眠
Linux内核地址映射
1.1明确
嵌入式处理器,CPU访问外设都是以地址指针的形式访问,也就是要访问外设,必须知道这个外设的物理地址
在Linux系统中,不管是在用户空间还是在内核空间,也就是不管应用程序还是驱动程序一律不允许直接访问外设的物理地址。要想访问需要提前将物理地址映射到内核虚拟地址或用户虚拟地址上,将来程序访问用户虚拟地址或者内核虚拟地址就是在访问物理地址。
Linux系统4G虚拟地址空间划分:
用户虚拟地址:(3G)0X00000000~0XBFFFFFFFF
内核虚拟地址:(1G)0XC0000000~0XFFFFFFFFF
如何将物理地址映射到内核虚拟地址上
利用ioremap函数
如何将物理地址映射到用户虚拟地址上
利用mmap函数
1.2 ioremap函数详解
函数原型:
void *ioremap(unsigned long pyhs_base_addr,unsigned long size)
函数功能:将外设的物理地址映射到内核虚拟地址上
pyhs_base_addr:传递要映射的外设的起始物理地址
size:传递要映射的外设物理地址空间的大小
例如:一个寄存器的物理地址空间为4字节
返回值:返回映射的内核起始虚拟地址
结论:一旦映射完毕,将来访问映射的内核虚拟地址就是在访问物理地址。
void iounmap(void *vir_base_addr)
函数功能:解除物理地址,不使用了,还不解除,内存浪费
vir_base_addr:传递映射的内存起始虚拟地址
参考代码:
寄存器 物理地址
GPIODATA 0XE0200084
GPIOCON 0XE0200080
地址映射:
unsigned long *gpiocon;
unsigned long *gpiodata;
gpiocon = ioremap(0XE0200080,4);
gpiodata = ioremap(0XE0200084,4);
//解除地址映射
iounmap(gpiocon);
iounmap(gpiodata);
由于物理地址空间都是连续的,所以还可以连续映射
void *gpio_base;
unsigned long *gpiocon;
unsigned long *gpiodata;
gpio_base = ioremap(0XC001C000,24)
gpiocon = (unsigned long *)(gpio_base+0X00);
gpiodata = (unsigned long *)(gpio_base+0X04);
//解除地址映射
iounmap(gpio_base);
3、如何将物理地址映射到用户虚拟地址(0X00000000~0XBFFFFFFF)上呢
利用mmap函数
回顾mmap系统调用函数用户3G虚拟地址空间的划分(低地址->高地址)
代码段->数据段-BSS段->堆区->MMAP虚拟内存映射区->栈区
代码 全局 全局 malloc
mmap 局部
3.1回顾mmap系统调用函数
void *mmap(void *addr,size_t length,int prot,int flag,int fd,off_t offset);
函数功能:将文件映射到用户空间的MMAP虚拟内存映射区的虚拟地址上,一旦映射完成,访问映射的用户虚拟空间就在访问文件
参数:addr:必须给NULL,让内核帮你在MMAP虚拟内存映射区中找一块空闲的虚拟内存区域来映射文件。
length:指定空闲的用户虚拟内存区域的大小
prot:对空闲的用户虚拟内存的访问权限,PROT_READ,PROT_WRITE
flag:标志,MMAP_SHARED
fd:指定要映射的文件
offset:从文件哪个地方开始映射,偏移量
返回值:内核把空闲的用户虚拟内存区域的首地址进行返回,将来访问这个虚拟地址空间就是在访问文件
例如:
void *addr;
int fd = open(a.txt);
//映射
addr = mmap(NULL,0X1000,PROT_READ|PROT_WRITE,MMAP_SHARED,fd,0);
//表面上看向用户虚拟内存写入字符串,实际是向文件写入字符串
memcpy(addr,"hello world",12);
透过现象看本质:与其说是mmap将文件映射到用户虚拟地址空间中,不如说是将存储文件数据的外设的物理地址和用户虚拟地址进行映射,
一旦完成这种映射,将来以地址指针的形式访问映射的用户虚拟地址就是在访问对应的物理地址。
3.3mmap系统调用的实现过程
1、应用程序调用mmap,首先调用C库的mmap
2、C库的mmap做两件事:
(1)保存mmap调用号到R7寄存器
(2)调用swi/svc指令触发软中断异常
3、一旦触发软中断异常,CPU核无条件的处理软中断异常
3.1CPU核首先硬件上做四件事
(1)备份CPSR到SPSR_SVC
(2)设置CPSR的BIT[7:0]=11010011
(3)保存返回地址LR_SVC=PC-4
(4)设置PC=0X80
至此开启软件处理软中断异常的流程
3.2软件处理软中断流程四步骤:
(1)提前建立异常向量表
而Linux系统的异常向量表位于内核中,所以也就是进程此时“陷入”内核空间继续运行
(2)保护现场
(3)调用软中断处理函数,内核的软中断处理函数做三件事:
a:从R7寄存器取出mmap系统调用号
b:根据系统调用号在系统调用表中找到对应的内核函数sys_mmap
c:一旦找到立马执行sys_mmap
(4)内核的sys_mmap做三件事
a:内核首先在当前进程的MMAP虚拟内存映射区中找一块空闲的用户虚拟内存区域,将来用于和物理地址做映射(找空 闲区的算法一般RB_Tree)
b:一旦找到,并且应用程序给mmap函数传递了参数(大小、PROT_READ|PROT_WRITE...),并且本身空闲的用户虚 拟内存区域势必有起始地址,这些都是在描述空闲的用户虚拟内存的属性,此时内核用struct vm_area_struct数据结构 定义初始化一个对象来描述,内核帮你找的空闲的用户虚拟内存属性
struct vm_area_struct
{
unsigned long vm_start;
unsigned long vm_end;
pgprot_t vm_page_prot;
unsigned long vm_flag;
unsigned long vm_pgoff;
....
}
vm_start:描述空闲的用户虚拟内存的起始地址 等于mmap系统调用函数的返回值addr
vm_end:描述空闲的用户虚拟内存的结束地址 vm_end = vm_start + 大小
vm_page_prot:描述空闲的用户虚拟内存的读写权限 = PROT_READ|PROT_WRITE
vm_flag:其余属性 = MAP_SHARED
vm_pgoff:偏移量 = 0
c:内核的sys_mmap最后调用底层驱动的mmap接口,内核sys_mmap将第二步创建的vm_area_struct对象的地址传递给 底层驱动的mmap接口,也就是将来底层驱动的mmap接口通过对象指针能够获得到空闲的用户虚拟内存的各种属性 (起始地址、结束地址、访问权限等)
d:底层驱动的mmap接口只做一件事:将已知的用户虚拟内存空间和已知的外设的物理地址空间做映射即可,一旦完成映 射,将来硬件的操作访问都是应用程序完成。底层驱动mmap接口类似中介。
e:底层驱动mmap接口调用完毕,然后就是各种恢复现场,状态恢复,跳转返回,至此mmap调用完毕,接下来在用户空 间的应用程序中访问映射的用户虚拟地址就是在访问物理地址
3.3底层驱动的mmap接口
struct file_operations
{
int (*mmap)(struct file *file,struct vm_area_struct *vma)
}
接口功能:只做一件事,将已知的用户虚拟地址和已知的物理地址做映射
file:文件指针,和fd有关
vma:指向内核sys_mmap创建的vm_area_struct对象,此属性描述内核帮你找的空闲用户虚拟内存的属性,
底层驱动的mmap接口利用vma获取已知的用户虚拟内存的各种属性
如何完成最终的映射呢?
调用以下函数即可完成映射:
int remap_pfn_range(struct vm_area_struct *vma,unsigned long addr,unsigned long pfn,unsigned long size,pgprot_t prot)
功能:给mmap接口使用,用于建立映射关系
参数:
vma:传递mmap接口的第二个参数vma指针即可
addr:传递已知的空闲的用户虚拟起始地址,就是vma->vm_start
pfn:船底已知要映射的起始物理地址
注意:物理地址要右移12位
例如:pfn = 0XC001C000>>12
切记:这个物理地址必须是页面大小的整数倍,一页 = 4KB = 0X1000
例如:物理地址0XC001C000做映射合法
物理地址:0XC001C004做映射不合法
size:传递用户虚拟内存的大小
就是vma->vm_end-vma->vm_start
prot:传递用户虚拟内存的访问权限
就是vma->vm_page_prot
4、read/write/ioctl和mmap的对比
(1)不管是哪种方法,最终的目的都是实现和硬件进行数据交互
(2)read/ioctl对硬件设备数据读操作流程
1)先从硬件读取数据放到内核缓冲区
2)再从内核缓冲区拷贝到用户缓冲区
3)硬件->内核缓冲区->用户缓冲区,要经过两次拷贝
(3)write/ioctl对硬件设备数据写操作流程
1)先从用户缓冲区拷贝数据到内核缓冲区
2)再从内核缓冲区写到硬件
3)用户缓冲区->内核缓冲区->硬件,要经过两次拷贝
(4)mmap对硬件设备数据读写操作流程
读取数据
硬件->用户缓冲区 一次拷贝
写入数据
用户缓冲区->硬件 一次拷贝
(5)结论:
1)read/write/ioctl:数据操作要经过两次数据拷贝,如果操作的数据量较小,对系统性能的影响几乎可以忽略不计,例 如开关灯等如果操作的数据量比较大,对系统的影响非常致命。例如LCD显示屏,摄像头,声卡,网络等
2)如果操作数据量比较大,必然采用mmap接口,将两次数据拷贝变成一次,但是mmap致命缺点是操作的用户虚拟内 存和物理内存必须是页面大小的整数倍,如果访问的数据量比较小,用mmap最终浪费宝贵的内存资源
现象./led_test on没有立即打开
1、mmap有应用缓冲,不能立即更新寄存器volatile
切记:只要利用mmap对GPIO进行输入、输出操作,都要将cache功能关闭掉,就一条语句:
vma->vm_page_propgprot_noncached(vma->vm_page_prot);