前言:
整理 linux 相关基础知识点,参考文献:linux 字符设备驱动详解、LDD3、韦东山驱动大全/linux 应用开发完全手册 4.0、文中提到的相关博客
特别说明:这篇文章为摘抄总结,非原创文章,如果内容涉及侵权请及时告知,作者会立即删除相关内容。
一、字符设备
字符设备是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等,而字符设备用到的核心数据结构有file 、inode 、cdev 、file_operations 。
1、struct file 数据结构
struct file 结构与用户空间程序中的FILE结构没有任何关联 ,FILE结构在 C 库中定义不会出现在内核代码中,struct file 是一个内核结构,它不会出现在用户程序中。struct file 结构代表一个打开的文件(它不仅仅限定于设备驱动程序,系统中每个打开的文件在内核空间都有一个对应的 file 结构 )。它由内核在open时创建,并且传递给在该文件上操作的所有函数,直到最后的close函数,在文件的所有实例都被关闭后,内核会释放这个数据结构。在内核和驱动源代码中,struct file 的指针通常被命名为 file 或 filp ,为了不和这个结构本身混淆,我们一致将指向该结构的指针称为 filp,file 则为结构本身。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 struct file { union { struct llist_node fu_llist ; struct rcu_head fu_rcuhead ; } f_u; struct path f_path ; #define f_dentry f_path.dentry struct inode *f_inode ; const struct file_operations *f_op ; spinlock_t f_lock; atomic_long_t f_count; unsigned int f_flags; fmode_t f_mode; struct mutex f_pos_lock ; 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 void *private_data; #ifdef CONFiG_EPOLL struct list_head f_ep_links ; struct list_head f_tfile_llink ; #endif struct address_space *f_mapping ; } __attribute__((aligned(4 )));
这个数据结构中驱动相关的几个重要成员变量如下表所示:
重要成员
说明
loff_t f_pos
当前读/写位置。loff_t有64位,驱动程序要知道文件中的当前位置,可以读取这个值,但不要去修改它。read/write会使用他们接收到的最后那个指针参数来更新这一位置,而不是直接针对filp->f_pos进行操作。这一规则的一个例外是llseek方法,该方法的目的本身就是为了修改文件位置
unsigned int f_flags
文件标志,如O_RDONLY、O_NONBLOCK、O_SYNC,检查用户的请求是否是非阻塞式的操作,驱动程序需要检查O_NONBLOCK标志,而其他标志很少用到。注意:检测读写权限应该使用f_mode而不是f_flags。所有这些标志都被定义在<linux/fcntl.h>中
struct file_operations *f_op;
与文件相关的操作。内核在执行open操作时对这个指针赋值,以后需要处理这个操作时就读取这个指针。
void *private_data;
file 结构的私有数据,被初始化为NULL
2、struct inode 结构体
内核用 inode 结构在内部表示文件,因此它和 file 结构不同,后者表示打开的文件描述。对单个文件可能有多个打开的 file 文件描述 (上层可以多次 open 一个文件),但他们都指向同一个 inode 文件 。inode 包含文件访问权限、属主、组、大小、生成时间、访问时间、最后修改时间等大量文件信息。部分数据结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 struct inode { ... umode_t i_mode; uid_t i_uid; gid_t i_gid; dev_t i_rdev; loff_t i_size; struct timespec i_atime ; struct timespec i_mtime ; struct timespec i_ctime ; unsigned int i_blkbits; blkcnt_t i_blocks; union { struct pipe_inode_info *i_pipe ; struct block_device *i_bdev ; struct cdev *i_cdev ; } ... };
其中驱动对驱动编程有用的成员变量只有两个如下:
成员变量
含义
dev_t i_rdev
当 inode 结构描述的文件为设备文件时,表示它的设备号
struct cdev *i_cdev
当 inode 指向一个字符设备文件时,i_cdev 为其对应的 cdev 结构体指针
在驱动中也可通过i_rdev获取设备号,内核提供了下面两个函数来获取 inode 结构中 i_rdev 字段中的设备号:
1 2 unsigned int imajor (struct inode* inode) ; unsigned int iminor (struct inode* inode) ;
3、struct file_operations 结构体
该结构体是系统调用与驱动连接的桥梁,当我们在应用层使用 open 函数打开一个设备的时候,内核会创建一个 file 结构并关联 file_operations 中的一组函数,最终会调用到驱动中关联的 file_operations 结构体实例中 open 函数。而 file_operations 定义了一组操作函数,我们不一定全部用到,通常用到什么函数就关联什么函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 struct file_operations { struct module *owner ; loff_t (*llseek) (struct file *, loff_t , int ); ssize_t (*read) (struct file *, char __user *, size_t , loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t , loff_t *); ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long , loff_t ); ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long , loff_t ); int (*iterate) (struct file *, struct dir_context *); unsigned int (*poll) (struct file *, struct poll_table_struct *) ; long (*unlocked_ioctl) (struct file *, unsigned int , unsigned long ); long (*compat_ioctl) (struct file *, unsigned int , unsigned long ); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); ...... };
1)open() 函数
对设备文件进行的第一个操作,如果这个函数没有实现,当用户调用 open() 时,一直显示成功,但是你的驱动不会得到通知。open 函数提供给驱动程序以初始化的能力,从而为以后的操作完成初始化做准备 。大部分驱动程序中应当完成下面工作。
检测设备特定的错误(注入设备未就绪或类似的硬件问题)
如果设备是首次打开,则对其进行初始化。
如果有必要,更新 fop 指针
分配并填写置于 filp->private_date 里的数据结构
函数接口
int (*open) (struct inode *inode , struct file *filp);
函数参数
参数含义
inode
为文件节点(详细见前面inode结构)
filp
指向内核创建的文件结构(详细见前面file结构)
3)read() 函数
用来从设备读取数据,成功时函数返回读取的字节数 ,返回值是一个 “signed size” 类型, 常常是目标平台本地的整数类型,出错时返回一个负值,用户调用 read() 时如果此函数未实现,将得到 -EINVAL 的返回值,它与用户空间的 fread() 函数对应。
函数接口
ssize_t (*read) (struct file *filp, char __user *buffer, size_t size , loff_t *ppos);
函数参数
参数含义
filp
指向内核创建的文件结构
buffer
数据返回给用户空间的内存地址
size
为要读取的信息长度,以字节为单位
ppos
为读的位置相对于文件开头的偏移,在读取信息后,这个指针一般都会移动,移动的值为要读取信息的长度值
4)write() 函数
向设备发送数据 ,成功时返回写入的字节数 ,如果此函数未实现,当用户调用 write() 时,将得到 -EINVAL 的返回值,它与用户空间的 fwrite() 函数对应
函数接口
ssize_t (*write) (struct file * filp, const char __user *buffer, size_t size, loff_t * ppos);
函数参数
参数含义
filp
指向系统open时内核创建的文件结构
buffe
用户要写入文件的信息缓冲区
size
要写入信息的长度
ppos
当前的读/写位置,这个值通常是用来判断写文件是否越界
注:这个操作和上面的对文件进行读的操作均为阻塞操作
4)ioctl() 函数
提供设备相关控制命令的实现(既不是读操作也不是写操作),当调用成功时,返回给调用程序一个非负值。如果设备不提供 ioctl 入口点,则对于任何内核未预先定义的请求,ioctl 系统调用将返回错误。(-ENOTTY,“No such ioctl for device, 该设备无此iotcl命令”)。
函数接口
int (*ioctl) (struct inode *inode, struct file *flip, unsigned int cmd, unsigned long arg);
函数参数
参数含义
inode
为文件节点
filp
指向系统open时内核创建的文件结构
cmd
从用户那里不改变地传下来的命令
arg
对应命令的参数
为了保证用户传递下来的命令安全可靠,内核定义了iotcl命令的组成方式,当使用这个函数是应当遵循这个规则。
a. 命令的组成
设备类型 由 8 位组成,可以是 0 ~ 0xFF 之间的值.
方向 由2位组成,表示数据传输的方向,下表给出可选方向。
参数
参数含义
_IOC_NONE
无数据传输
_IOC_READ
读操作
_IOC_WRITE
写操作
_IOC_READ | _IOC_WRITE
双向操作
数据长度 字段表示涉及的用户数据的大小,这个成员的宽度依赖于体系结构,通常是 13 位或者14 位。
b. 命令生成方式
_IO() : 用于生成不涉及数据传输的简单命令
1 #define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)
_IOR() : 用于生成读操作命令
1 2 #define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),\ (_IOC_TYPECHECK(size)))
_IOW() : 用于生成写操作命令
1 2 #define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),\ (_IOC_TYPECHECK(size)))
_IOWR() : 用于生成即可读也可写的操作命令
1 2 3 #define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr), \ (_IOC_TYPECHECK(size)))
_IO、_IOR 等使用的_IOC 宏 :
1 2 3 4 5 #define _IOC(dir,type,nr,size) \ (((dir) << _IOC_DIRSHIFT) | \ ((type) << _IOC_TYPESHIFT) | \ ((nr) << _IOC_NRSHIFT) | \ ((size) << _IOC_SIZESHIFT))
c. 简单使用
下面的代码为伪代码,主要用于说明使用方式
1 2 3 4 #define DEV_NAME 'D' #define DEV_CMD _IO( DEV_NAME, 0) #define DEV_CMD_READ_SOMTHING _IOR(DEV_NAME, 1, unsigned int) #define DEV_CMD_WRITE_SOMTHING _IOW(DEV_NAME, 2, unsigned int)
5)llseek() 函数
用来修改一个文件当前读写位置,并将新位置返回,出错时这个函数返回一个负值。如果这个函数指针是NULL,对 llseek 的调用将会以某种不可预期的方式修改 file 结构中的位置计数器。
函数接口
loff_t (*llseek) (struct file *filp , loff_t p, int orig);
filp
指向系统open时内核创建的文件结构
p
当前的读/写位置,这个值通常是用来判断写文件是否越界
orig
文件定位的地址,文件开头(SEEK_SET、0),当前位置(SEEK_CUR、1),文件末尾(SEEK_END、2)
更多请参考博客Ph_one 以及书籍linux设备驱动开发详解
4、struct cdev 结构体
在当前的 liux 内核中每一个字符设备都有一个 cdev 描述,即一个 cdev 结构表示一个字符设备。
1 2 3 4 5 6 7 8 struct cdev { struct kobject kobj ; struct module *owner ; struct file_operations *ops ; struct list_head list ; dev_t dev; unsigned int count; };
利用这个数据结构就可以在内核创建一个字符设备,分配和初始化 cdev 的方式有两种,第一种是直接利用同 struct cdev 类型创建,除此之外还可以使用运行时使用动态创建以及释放,函数接口如下表所示
函数接口
函数功能
struct cdev *cdev_alloc(void)
动态申请一个 cdev 内存。
void cdev_put(struct cdev *p)
释放 cdev_alloc 申请的内存
5、字符设备的注册与卸载
获得cdev这个结构之后我们还需将其初始化以及注册到内核。
1) cdev_init() 函数
1 2 3 4 5 6 7 8 void cdev_init (struct cdev *cdev, const struct file_operations *fops) { memset (cdev, 0 , sizeof *cdev); INIT_LIST_HEAD(&cdev->list ); cdev->kobj.ktype = &ktype_cdev_default; kobject_init(&cdev->kobj); cdev->ops = fops }
2) cdev_add() 函数
1 2 3 4 5 6 7 8 int cdev_add (struct cdev *p, dev_t dev, unsigned count) { p->dev = dev; p->count = count; return kobj_map(cdev_map, dev, count, NULL , exact_match, exact_lock, p); }
3) cdev_del() 函数
1 2 3 4 5 void cdev_del (struct cdev *p) { cdev_unmap(p->dev, p->count); kobject_put(&p->kobj); }
向系统删除一个cdev,完成字符设备的卸载,对 cdev_del 的调用通常发生在字符设备驱动模块卸载函数中,当 cdev_del 被调用后对应的cdev结构就不应被访问了。
4) register_chrdev() 函数
函数接口
int register_chrdev(unsigned int major, const char* name, struct file_operations* fops)
函数参数
参数含义
major
设备的主设备号
name
驱动程序的名称
fops
file_operations 结构
调用这个函数将为给定的主设备号注册 0 ~ 255 作为次设备号,并为每个设备建立一个对应默认 cdev 结构。使用这一接口的驱动程序必须能够处理所有 256 个次设备号的 open 调用,而且也不能使用大于 255 的主设备号和次设备号。
5) unregister_chrdev() 函数
函数接口
int unregister_chrdev(unsigned int major, const char* name)
函数参数
参数含义
major
要卸载的设备的主设备号
name
要卸载驱动程序的名称
6、设备号
字符设备在应用空间的表示形式为 /dev/*** ,/dev 下的文件被称为设备节点,设备节点怎么和字符设备建立联系呢?答案就在设备号,通过设备号来建立设备节点与字符设备的联系 。设备号由主设备号 和次设备号 组成
1 2 3 4 5 6 7 8 9 #include <uapi/linux/kdev_t.h> #define MINORBITS 20 #define MINORMASK ((1U << MINORBITS) - 1) #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
设备号由 unsingd int 表示,其中低 20 位表示次设备号,剩下的 12 位表示主设备号。实际上并没用到所有的 12 位,在老式内核中主设备号的范围为 0-255。后来不够用了于是对其进行了扩展。
1 2 3 4 5 6 7 #define CHRDEV_MAJOR_MAX 512 #define CHRDEV_MAJOR_DYN_END 234 #define CHRDEV_MAJOR_DYN_EXT_START 511 #define CHRDEV_MAJOR_DYN_EXT_END 384
范围
分配方式
0-233
静态分配
234-255
动态分配
256-383
静态分配
384-511
动态分配
动态分配优先使用 256-383 之间的设备号,如果没有了再使用 384-511 之间的设备号。对于这两种设备号的分配内核提供了下面接口
1 2 3 4 5 6 7 8 9 10 11 12 int register_chrdev_region (dev_t from, unsigned count, const char *name) int alloc_chrdev_region (dev_t *dev, unsigned baseminor, unsigned count,const char *name)
应用程序到内核空间的调用流程如下
应用层使用 open、write、read 函数操作 dev 目录下的设备文件
设备文件通过内核中对应设备的设备号,通过设备号找到对应的 file_operations 结构
调用 file_operations 结构中对应的 open、write、read 函 数驱动相关硬件进行操作
有了设备号怎么创建设备节点呢,有两种方式一种是手动创建
1 2 3 4 5 mknod /dev/xyz c(表示是字符设备) 主设备号 次设备号 ls -l /dev/xyz
除此之外还可以在驱动中自动创建设备文件,该方式依赖于设备模型,字符设备和设备模型的通过设备号关联在一起,只要有设备号就会在 /dev/ 下创建设备节点 ,参考设备驱动模型 ,在这篇文章中搜索 devtmpfs_create_node 。,一般情况下我们通常按照下面步骤创建
调用class_create()函数,可以用它来创建一个类,这个类存放于sys/class/下面,
再调用 device_create() 函数来在/dev目录下创建相应的设备节点,同时也会在sys/class/下创建出对应的设备文件
1 2 3 4 5 6 7 8 9 10 struct class *class_create (struct module *owner, const char *name) ;struct device *device_create (struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...)
当卸载设备的时候使用函数 device_destroy() 从 linux 内核系统设备驱动程序模型中移除一个设备,并删除 /sys/devices/virtual 目录下对应的设备目录及 /dev/ 目录下对应的设备文
1 2 3 void device_destroy (struct class *dev, dev_t devt) ;
7、内核与用户空间数据交互
用户空间与内核空间之间的数据不能直接进行简单的赋值交互 ,linux 内核提供了两者交互的函数通过 copy_from_user 获取用户空间的数据,通过 copy_to_user 将内核空间的数据传给用户空间,当操作成功后均返回 0,操作失败返回负值。
1) copy_from_user
1 2 3 4 5 6 7 8 9 10 static __always_inline unsigned long __must_check copy_from_user (void *to, const void __user *from, unsigned long n) { if (likely(check_copy_size(to, n, false ))) n = _copy_from_user(to, from, n); return n; }
2) copy_to_user
1 2 3 4 5 6 7 8 9 10 static __always_inline unsigned long __must_checkcopy_to_user (void __user *to, const void *from, unsigned long n) { if (likely(check_copy_size(from, n, true ))) n = _copy_to_user(to, from, n); return n; }
8、编程验证
内核部分 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 #include <linux/fs.h> #include <linux/slab.h> #include <linux/cdev.h> #include <linux/device.h> #include <linux/module.h> #include <linux/version.h> #include <linux/uaccess.h> #define SIZE 256 #define DEV_NAME 'M' #define DEV_CLEAN _IO( DEV_NAME, 0) struct mycdev { char buf[SIZE]; dev_t dev; struct cdev * cdev ; struct file_operations * fops ; struct class * class ; struct device device ; }; struct mycdev * mycdev = NULL ;ssize_t mycdev_open (struct inode *inode, struct file *filp) { printk("mycdev open\n" ); filp->private_data = mycdev; return 0 ; } ssize_t mycdev_read ( struct file *filp, char __user *buf, size_t size, loff_t *ppos) { int err = 0 ; unsigned int pos = *ppos; unsigned int count = size; struct mycdev * mycdev = filp->private_data; if (pos > SIZE){ return count ? -ENXIO : 0 ; } if (count > SIZE - pos){ count = SIZE - pos; } printk("read begin, pos:%d, count:%d \n" ,pos, count); err = copy_to_user(buf, (void *)((char *)mycdev->buf + pos), count); if (err){ printk("copy to user err\n" ); return -EPERM; } *ppos += count; printk("read %d bytes from mydev->buf[%d]\n" , count, pos); return count; } static ssize_t mycdev_write ( struct file *filp, const char __user *buf, size_t size, loff_t *ppos) { int err = 0 ; unsigned int pos = *ppos; unsigned int count = size; struct mycdev * mycdev = filp->private_data; if (pos > SIZE){ return count ? -ENXIO : 0 ; } if (count > SIZE - pos){ count = SIZE - pos; } printk("write begin, pos:%d, count:%d \n" ,pos, count); err = copy_from_user(mycdev->buf + pos, buf, count); if (err){ printk("copy from user err\n" ); return -EFAULT; } *ppos += count; printk("write %d bytes to mydev->buf[%d]\n" , count, pos); return count; } static loff_t mycdev_llseek (struct file *filp, loff_t offset, int orig) { if (offset < 0 ){ return -EINVAL; } if (0 == orig){ filp->f_pos = offset; }else if (1 == orig){ filp->f_pos += offset; }else if (2 == orig){ filp->f_pos = SIZE + offset; } return filp->f_pos; } static long mycdev_ioctl (struct file *filp, unsigned int cmd, unsigned long arg) { struct mycdev * mycdev = filp->private_data; switch (cmd){ case DEV_CLEAN: memset (mycdev->buf, 0 , SIZE); break ; default : printk("don't have this cmd\n" ); return -EINVAL; } return 0 ; } static int mycdev_release (struct inode *inode, struct file *filp) { printk("mycdev release\n" ); return 0 ; } static struct file_operations mycdev_fops = { .owner = THIS_MODULE, .open = mycdev_open, .read = mycdev_read, .write = mycdev_write, .llseek = mycdev_llseek, .release = mycdev_release, #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 36) .ioctl = mycdev_ioctl, #else .unlocked_ioctl = mycdev_ioctl, .compat_ioctl = mycdev_ioctl, #endif }; int mycdev_init (struct mycdev* mycdev) { int err = 0 ; if (!mycdev || !mycdev->fops){ printk("mycdev or mycdev->fops is null\n" ); goto out; } err = alloc_chrdev_region(&mycdev->dev, 0 , 1 , "mycdev" ); if (err < 0 ){ printk("alloc chrdev err\n" ); goto alloc_chrdev_erro; } mycdev->cdev = cdev_alloc(); if (!mycdev->cdev){ printk("cdev alloc err\n" ); goto mycdev_alloc_erro; } cdev_init(mycdev->cdev, mycdev->fops); mycdev->cdev->owner = THIS_MODULE; return 0 ; mycdev_alloc_erro: unregister_chrdev_region(mycdev->dev,1 ); alloc_chrdev_erro: out: return -ENOMEM; } void mycdev_device_release (struct device *dev) { printk("mycdev device release\n" ); } int mycdev_creat_sysfs (struct mycdev* mycdev) { int err; mycdev->class = class_create(THIS_MODULE, "mycdev_cls" ); if (!mycdev->class){ return -ENOMEM; } mycdev->device.class = mycdev->class; mycdev->device.init_name = "mycdev" ; mycdev->device.devt = mycdev->dev; mycdev->device.release = mycdev_device_release; err = device_register(&mycdev->device); if (err < 0 ){ printk("device register err\n" ); return err; } return 0 ; } static int __init hello_init (void ) { int ret = 0 ; mycdev = (struct mycdev*)kzalloc(sizeof (*mycdev), GFP_KERNEL); if (!mycdev){ printk("mycdev alloc err\n" ); return -ENOMEM; } mycdev->fops = &mycdev_fops; ret = mycdev_init(mycdev); if (ret < 0 ){ printk("mycdev init err\n" ); return ret; } ret = cdev_add(mycdev->cdev,mycdev->dev,1 ); if (ret < 0 ) { printk("cdev add err %d\n" ,ret); return ret; } ret = mycdev_creat_sysfs(mycdev); if (ret < 0 ){ printk("mycdev creat sysfs err\n" ); return ret; } return ret; } static void __exit hello_exit (void ) { printk("mycdev exit\n" ); cdev_del(mycdev->cdev); device_unregister(&mycdev->device); class_destroy(mycdev->class); unregister_chrdev_region(mycdev->dev,1 ); kfree(mycdev); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL" ); MODULE_AUTHOR("baron-z" ); MODULE_DESCRIPTION("my cdev test driver" );
应用程序部分 :
这部分主要用于验证内核实现,不具备严谨性,只是测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #include <stdio.h> #include <sys/ioctl.h> #include <fcntl.h> #include <sys/types.h> #include <unistd.h> #include <malloc.h> #include <string.h> #define DEV_NAME 'M' #define DEV_CLEAN _IO( DEV_NAME, 0) int main (int argc, char * argv[]) { int size = 0 ; char * buff = (char *)malloc (255 ); int fd = -1 ; buff[254 ] = '\0' ; if (argc < 2 ) return 0 ; fd = open("/dev/mycdev" , O_RDWR); if (fd < 0 ){ printf ("open /dev/mycdev err\n" ); return -1 ; } if (!strcmp ("write" , argv[1 ])){ write(fd, argv[2 ], strlen (argv[2 ])); printf ("write %s to mycdev buf\n\n" , argv[2 ]); }else if (!strcmp ("read" , argv[1 ])){ read(fd, buff, 255 ); printf ("read data form mycdev : %s\n\n" , buff); }else if (!strcmp ("clean" , argv[1 ])){ ioctl(fd, DEV_CLEAN); printf ("clean mycdev buf\n\n" ); } close(fd); return 0 ; }
验证结果:
1 2 3 4 5 6 7 8 9 10 11 [root@100 ask:~]# ./a.out write 1234556 write 1234556 to mycdev buf [root@100 ask:~]# [root@100 ask:~]# ./a.out read read data form mycdev : 1234556 [root@100 ask:~]# [root@100 ask:~]# ./a.out clean clean mycdev buf [root@100 ask:~]# [root@100 ask:~]# ./a.out read read data form mycdev :
二、同步与互斥
互斥 指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序 的。同步 指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序 访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。
参考【干货】同步与互斥的失败例子
1、原子变量
1) 基本概念
原子操作的基本单位,原子操作指的是由多步操作组成的一个操作。如果操作不能原子地执行,则要么执行完所有的步骤,要么一步也不执行,不可能执行所有步骤的一个子集。原子操作就是不能打断的操作 ,内核定义的原子变量结构如下:
1 2 3 4 5 kernel-4.4 /include/linux/types.h typedef struct { volatile int counter; } atomic_t ;
armv6 之前的架构 soc 不支持 smp,因此原子变量的实现非常简单,直接采用关中断的方式,如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 arch\arm\include\asm \atomic.h #define ATOMIC_OP(op, c_op, asm_op) \ static inline void atomic_##op(int i, atomic_t *v) \ { \ unsigned long flags; \ \ raw_local_irq_save(flags); \ v->counter c_op i; \ raw_local_irq_restore(flags); \ } \ #define ATOMIC_OP_RETURN(op, c_op, asm_op) \ static inline int atomic_##op##_return(int i, atomic_t *v) \ { \ unsigned long flags; \ int val; \ \ raw_local_irq_save(flags); \ v->counter c_op i; \ val = v->counter; \ raw_local_irq_restore(flags); \ \ return val; \ } #define ATOMIC_FETCH_OP(op, c_op, asm_op) \ static inline int atomic_fetch_##op(int i, atomic_t *v) \ { \ unsigned long flags; \ int val; \ \ raw_local_irq_save(flags); \ val = v->counter; \ v->counter c_op i; \ raw_local_irq_restore(flags); \ \ return val; \ } #define ATOMIC_OPS(op, c_op, asm_op) \ ATOMIC_OP(op, c_op, asm_op) \ ATOMIC_OP_RETURN(op, c_op, asm_op) \ ATOMIC_FETCH_OP(op, c_op, asm_op)
smp 系统则是使用内联汇编来实现的 。对于 atomic_read 和 atomic_set 这些操作都只需要一条汇编指令,他们本身就是原子的。而 atomic_add 等操作,需要读出,修改,写入则需要特别处理,以 atomic_add 为例说明。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 arch\arm\include\asm \atomic.h #if __LINUX_ARM_ARCH__ >= 6 #define ATOMIC_OP(op, c_op, asm_op) static inline void atomic_##op(int i, atomic_t *v) { unsigned long tmp; int result; prefetchw(&v->counter); __asm__ __volatile__("@ atomic_" #op "\n" "1: ldrex %0, [%3]\n" " " #asm_op " %0, %0, %4\n" " strex %1, %0, [%3]\n" " teq %1, #0\n" " bne 1b" : "=&r" (result), "=&r" (tmp), "+Qo" (v->counter) : "r" (&v->counter), "Ir" (i) : "cc" ); } #define ATOMIC_OPS(op, c_op, asm_op) \ ATOMIC_OP(op, c_op, asm_op) \ ATOMIC_OP_RETURN(op, c_op, asm_op) \ ATOMIC_FETCH_OP(op, c_op, asm_op) ATOMIC_OPS(add) ATOMIC_OPS(sub)
原子操作的实现也主要依靠上述的两条指令 ldrex 和 strex
ldrex r0, [r1] : 读取 r1 所指内存的数据,存入 r0;
strex r2, r0, [r1] : 把 r0 的值写入 r1 所指内存,把 r2 设为 0 表示成功。如果没有写入则把 r2 设为 1 表示失败。
这两条指令怎么实现原子操作的呢,是通过 Local monitor 和 Global monitor 来实现的。Local monitor :对非共享内存和共享内存都会使用到,是对该 CPU 本地的内存进行标记 。 Global monitor : 对共享内存使用,也就是多个 CPU 都依赖于Global monitor 。
对于同一个 cpu 的多线程处理方式如下
thread 1
thread 2
local monitor的状态
Open Access state
LDREX
Exclusive Access state
LDREX
Exclusive Access state
Modify
Exclusive Access state
STREX
Open Access state
Modify
Open Access state
STREX
在 Open Access state 的状态下,执行 STREX 指令会导致该指令执行失败
保持Open Access state,直到下一个 LDREX 指令
刚开始默认情况 local monitor 的状态为 Open Access state
thread 1 运行 LDREX 这条指令将 local monitor 的状态修改为 Exclusive Access state。
后切换到 thread 2 执行 LDREX 这时检测到 local monitor 的状态为 Exclusive Access state
执行 Modify
执行 STREX,执行完 STREX 之后 local monitor 被设置为 Open Access state
回到 thread 1 执行 Modify
执行 STREX ,当执行 STREX 指令会导致该指令执行失败
结合前面的汇编实现。这时候 thread 1 又会重新执行 LDREX,Modify,STREX 直到执行成功。以此来实现原子操作。
可以总结出 ldrex 和 strex 的特性如下。
local monitor state
ldrex
strex
Open Access state
指令正常执行且修改 local monitor 状态为 Exclusive Access state
STREX 指令执行失败并返回 1
Exclusive Access state
指令正常执行且修改 local monitor 状态为 Exclusive Access state
STREX 正常执行且 local monitor 被设置为 Open Access state 同时返回 0
Global monitor 则用于多 cpu 之间的,状态和 local monitor 是一样的,这里就不再赘述了。
参考博客armv7实现原子操作的本质——ldrex和strex指令 、ARM同步指令之LDREX和STREX
2) 原子操作
函数接口
接口含义
atomic_t v = ATOMIC_INIT(0)
定义原子变量并初始化v为0
void atomic_set(atomic_t* v,int i)
设置原子变量的值为i
atomic_read(atomic_t* v)
返回原子变量的值
void atomic_add(int i, atomic_t* v)
原子变量增加i
void atomic_sub(int i, atomic_t* v)
原子变量减少i
void atomic_inc(atomic_t* v)
原子变量自增1
void atomic_dec(atomic_t* v)
源自变量自减1
int atomic_inc_and_test(atomic_t* v)
原子变量自增1,并测试其值是否为0。为0返回true,否则返回false
int atomic_dec_and_test(atomic_t* v)
原子变量自减1,并测试其值是否为0。为0返回true,否则返回false
int atomic_sub_and_test(int i, atomic_t* v)
原子变量减少i, 并测试其值是否为0。为0返回true,否则返回false
int atomic_add_return(int i, atomic_t* v)
原子变量的值增加i,并返回新的值
int atomic_sub_return(int i, atomic_t* v)
原子变量的值减少i,并返回新的值
int atomic_inc_return(atomic_t* v)
原子变量的值自增1,并返回新的值
int atomic_dec_return(atomic_t* v)
原子变量的值自减1,并返回新的值
void set_bit(nr,void* addr);
设置addr地址的第nr位,即将nr位置1
void clear_bit(nr, void* addr);
清除addr地址的nr位,即将nr位清0
void chang_bit(nr, void* addr);
将addr的nr为反置
test_bit(nr, void* addr);
返回addr的第nr位
int test_and_set_bit(nt, void* addr);
测试addr的nr位,再设置为1
int test_and_clear_bit(nr, void* addr);
测试addr的nr位,再清为0
int test_and_chang_bit(nr, void* addr);
测试addr的nr位,再反置nr位
2、linux 中的锁
linux 内核提供了很多类型的锁,他们可以分为两类,自旋锁(spinning lock) 、睡眠锁(sleeping lock) 。自旋锁就是无法获得锁时,不会休眠,会一直循环等待 。睡眠锁则是是无法获得锁时,当前线程就会休眠 。
1) 自旋锁 spin_lock
自旋锁的实现也需要考虑到两种情况,单 cpu 系统和多 cpu 系统(smp 系统)。对于不同的架构自旋锁的实现方式不同,arm 架构自旋锁的数据结构如下所示,其中实现自旋的关键就是 next 和 owner 两个变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 typedef struct spinlock { union { struct raw_spinlock rlock ; #ifdef CONFIG_DEBUG_LOCK_ALLOC # define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map)) struct { u8 __padding[LOCK_PADSIZE]; struct lockdep_map dep_map ; }; #endif }; } spinlock_t ; typedef struct raw_spinlock { arch_spinlock_t raw_lock; #ifdef CONFIG_GENERIC_LOCKBREAK unsigned int break_lock; #endif #ifdef CONFIG_DEBUG_SPINLOCK unsigned int magic, owner_cpu; void *owner; #endif #ifdef CONFIG_DEBUG_LOCK_ALLOC struct lockdep_map dep_map ; #endif } raw_spinlock_t ; typedef struct { volatile unsigned int lock; } arch_spinlock_t ; typedef struct { union { u32 slock; struct __raw_tickets { #ifdef __ARMEB__ u16 next; u16 owner; #else u16 owner; u16 next; #endif } tickets; }; } arch_spinlock_t ;
对于没有其他 CPU 的单 CPU 系统,如果内核不支持进程抢占(preempt),当前在内核态执行的线程也不可能被其他线程抢占,也就没有其他进程/线程。所以对于不支持进程抢占(preempt)的单 CPU 系统,spin_lock 是空函数,不需要做其他事情。
如果单 CPU 系统的内核支持进程抢占(preempt) ,即当前线程正在执行内核态函数时,它是有可能被别的线程抢占的。这时 spin_lock 的实现就是调用 preempt_disable() 函数,关闭 cpu 的进程抢占功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 static __always_inline void spin_lock (spinlock_t *lock) { raw_spin_lock(&lock->rlock); } #define raw_spin_lock(lock) _raw_spin_lock(lock) #define _raw_spin_lock(lock) __LOCK(lock) #define __LOCK(lock) \ do { preempt_disable(); ___LOCK(lock); } while (0 ) #define ___LOCK(lock) \ do { __acquire(lock); (void)(lock); } while (0) #ifdef __CHECKER__ #define __acquire(x) __context__(x,1) #else define __acquire(x) (void )0 #endif
对于 spin_lock_irq(),在 UP 系统中就退化为 local_irq_disable() 和 preempt_disable(),如下所示:
1 2 3 #define __LOCK_IRQ(lock) \ do { local_irq_disable(); __LOCK(lock); } while (0 )
对于多 cpu 系统内核怎么实现自旋锁的呢,先来看一下源码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 #define raw_spin_lock(lock) _raw_spin_lock(lock) static __always_inline void spin_lock (spinlock_t *lock) { raw_spin_lock(&lock->rlock); } void __lockfunc _raw_spin_lock(raw_spinlock_t *lock){ __raw_spin_lock(lock); } static inline void __raw_spin_lock(raw_spinlock_t *lock){ preempt_disable(); spin_acquire(&lock->dep_map, 0 , 0 , _RET_IP_); LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock); } #define spin_acquire(l, s, t, i) lock_acquire_exclusive(l, s, t, NULL, i) #define lock_acquire_exclusive(l, s, t, n, i) lock_acquire(l, s, t, 0, 1, n, i # define lock_acquire(l, s, t, r, c, n, i) do { } while (0) #define LOCK_CONTENDED(_lock, try, lock) \ do { \ if (!try(_lock)) { \ lock_contended(&(_lock)->dep_map, _RET_IP_); \ lock(_lock); \ } \ lock_acquired(&(_lock)->dep_map, _RET_IP_); \ } while (0 ) do { if (!do_raw_spin_trylock(lock)) { lock_contended(&(lock)->dep_map, _RET_IP_); do_raw_spin_lock(lock); } lock_acquired(&(lock)->dep_map, _RET_IP_); } while (0 ) tatic inline int do_raw_spin_trylock (raw_spinlock_t *lock) { return arch_spin_trylock(&(lock)->raw_lock); } static inline int arch_spin_trylock (arch_spinlock_t *lock) { return !test_and_set_bit(0 , &lock->lock); } void do_raw_spin_lock (raw_spinlock_t *lock) { debug_spin_lock_before(lock); arch_spin_lock(&lock->raw_lock); debug_spin_lock_after(lock); } static inline void arch_spin_lock (arch_spinlock_t *lock) { unsigned long tmp; u32 newval; arch_spinlock_t lockval; prefetchw(&lock->slock); __asm__ __volatile__( "1: ldrex %0, [%3]\n" " add %1, %0, %4\n" " strex %2, %1, [%3]\n" " teq %2, #0\n" " bne 1b" : "=&r" (lockval), "=&r" (newval), "=&r" (tmp) : "r" (&lock->slock), "I" (1 << TICKET_SHIFT) : "cc" ); while (lockval.tickets.next != lockval.tickets.owner) { wfe(); lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner); } smp_mb(); }
由源码可以得出 spin_lock 主要做了下面的事情
调用 preempt_disable 关闭进程间的抢占
使用 ldrex 和 strex 以实现操作的原子性
如果 lockval.tickets.next 不等于 lockval.tickets.owner 如果不相等,循环将 cpu 进入低功耗待机状态,等待一会之后更新一下 lock->tickets.owner 的状态。直到拿到锁为止
通过 lockval.tickets.next 实现了排队的操作,每一个 spinlock 都有一个独一无二 lockval.tickets.next ,后来的 spinlock 都是在前一个的基础上增加 1,lock->tickets.owner 则保存当前轮到谁来获取临界资源。当我们释放锁的时候只需要将 lock->tickets.owner ++,下一个等待的进程进程就能够拿到对应的锁了。
1 2 3 4 5 6 static inline void arch_spin_unlock (arch_spinlock_t *lock) { smp_mb(); lock->tickets.owner++; dsb_sev(); }
spin_lock 常用了接口如下
函数接口
接口含义
spin_lock_init
初始化自旋锁 lock
spin_lock
如果能够获得自旋锁则立刻返回,否则将在那里等待,直到该自旋锁的持有者释放。
spin_unlock
释放自旋锁 lock
spin_try_lock
尝试获得自旋锁,如果能立刻获得锁返回 true,否则立刻返回 false
spin_lock_bh/spin_unlock_bh
加锁时禁止下半部(软中断),解锁时使能下半部(软中断)
spin_lock_irq/spin_unlock_irq
加锁时禁止中断,解锁时使能中断
spin_lock_irqsave/spin_lock_restore
加锁时禁止并中断并记录状态,解锁时恢复中断为所记录的状态
2) 信号量
一个信号量本质上是一个整数值,它和一对函数联合使用,这对函数通常称为 P 和 V。希望进入临界区的进程将在相关信号量上调用 P;如果信号量的值大于 0,则该值会减1,而进程可以继续。相反,如果信号量的值为 0(或者更小),进程进入休眠并等待直到其他人释放该信号量。对信号量的解锁通过调用 V 来完成;该函数增加信号量的值,并在必要时唤醒等待的进程。当我们需要将信号量用于互斥时,只需将信号量的值设为 1。这样的信号量在任何给定时刻只能由单个线程拥有。内核定义的信号量如下
1 2 3 4 5 6 struct semaphore { raw_spinlock_t lock; unsigned int count; struct list_head wait_list ; };
可以看出信号量其实也使用了 raw_spinlock_t 这个结构体,来看看源码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 void down (struct semaphore *sem) { unsigned long flags; raw_spin_lock_irqsave(&sem->lock, flags); if (likely(sem->count > 0 )) sem->count--; else __down(sem); raw_spin_unlock_irqrestore(&sem->lock, flags); } EXPORT_SYMBOL(down); static noinline void __sched __down(struct semaphore *sem){ __down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT); } static inline int __sched __down_common(struct semaphore *sem, long state, long timeout) { struct task_struct *task = current; struct semaphore_waiter waiter ; list_add_tail(&waiter.list , &sem->wait_list); waiter.task = task; waiter.up = false ; for (;;) { if (signal_pending_state(state, task)) goto interrupted; if (unlikely(timeout <= 0 )) goto timed_out; __set_task_state(task, state); raw_spin_unlock_irq(&sem->lock); timeout = schedule_timeout(timeout); raw_spin_lock_irq(&sem->lock); if (waiter.up) return 0 ; } timed_out: list_del(&waiter.list ); return -ETIME; interrupted: list_del(&waiter.list ); return -EINTR; }
总的来说只有得到信号量的进程才能执行临界区的代码,当得不到信号量时,进程会进入休眠等待状态 。并将当前进程放入对应的链表中。当有信号量被释放时遍历这个链表,唤醒被休眠的进程继续执行。信号量释放的流程如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void up (struct semaphore *sem) { unsigned long flags; raw_spin_lock_irqsave(&sem->lock, flags); if (likely(list_empty(&sem->wait_list))) sem->count++; else __up(sem); raw_spin_unlock_irqrestore(&sem->lock, flags); } EXPORT_SYMBOL(up); static noinline void __sched __up(struct semaphore *sem){ struct semaphore_waiter *waiter = list_first_entry(&sem->wait_list, struct semaphore_waiter, list ); list_del(&waiter->list ); waiter->up = true ; wake_up_process(waiter->task); }
常用接口
函数接口
接口含义
DECLARE_MUTEX(name);
将信号量name初始化为1
DECLARE_MUTEX_LOCKED(name);
将信号量name初始化为0
void init_MUTEX(struct semaphore *sem)
将信号量sem初始化为1
void init_NUTEX_LOCKED(struct semaphore *sem);
将信号量sem初始化为0
void sema_init(struct semaphore* sem, int val);
初始化信号量的值为 val
void down(struct semaphore *sem);
递减信号量的值,获取不到是进程进入休眠
int down_interruptible(struct semaphore *sem);
递减信号量的值,获取不到是进程进入休眠,但是操作是可以中断的,它允许一个在等待的信号量的用户空间进程被用户中断。如果在操作中被中断,函数会返回一个非零值,并且调用者不持有信号量,真确的使用它需要一直检查返回值并针对性地响应。
int down_trylock(struct seamphore* sem);
永远不会休眠,如果信号量在调用时不可获得,它会返回一个非零值
void up(struct semaphore *sem);
当队列中没有需要获取信号量的进程时释放信号量,如果有则唤醒队列中的第一个线程同时将信号量的持有转交给该线程
任何拿到信号量的线程都必须通过一次对 up 的调用而释放该信号量。在出现错误的情况下,经常需要特别小心。如果在拥有一个信号量时发生错误,必须将错误状态返回给调用者之前释放该信号量。忘记释放信号量将导致进程在某些无关的位置被意外挂起,很难复现和跟踪
3) 互斥锁
互斥锁主要用于实现内核中的互斥访问功能。一般用来保护一段代码,的数据结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 struct mutex { atomic_t count; spinlock_t wait_lock; struct list_head wait_list ; #if defined(CONFIG_DEBUG_MUTEXES) || defined(CONFIG_MUTEX_SPIN_ON_OWNER) struct task_struct *owner ; #endif #ifdef CONFIG_MUTEX_SPIN_ON_OWNER struct optimistic_spin_queue osq ; #endif #ifdef CONFIG_DEBUG_MUTEXES void *magic; #endif #ifdef CONFIG_DEBUG_LOCK_ALLOC struct lockdep_map dep_map ; #endif };
mutex 的实现分为两个部分 fastpath 和 slowpath。如果 fastpath 成功就不再使用 slowpath。同样的 mutex 的实现在不同的架构下实现方式也不同
1 2 3 4 5 6 7 #if __LINUX_ARM_ARCH__ < 6 #include <asm-generic/mutex-xchg.h> #else #include <asm-generic/mutex-dec.h> #endif #endif
armv6 之前的实现就不贴出来了,基本差不多。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 void __sched mutex_lock (struct mutex *lock) { might_sleep(); __mutex_fastpath_lock(&lock->count, __mutex_lock_slowpath); mutex_set_owner(lock); } static inline void __mutex_fastpath_lock(atomic_t *count, void (*fail_fn)(atomic_t *)) { if (unlikely(atomic_dec_return_acquire(count) < 0 )) fail_fn(count); } __mutex_lock_slowpath(atomic_t *lock_count) { struct mutex *lock = container_of(lock_count, struct mutex, count); __mutex_lock_common(lock, TASK_UNINTERRUPTIBLE, 0 , NULL , _RET_IP_, NULL , 0 ); } static __always_inline int __sched__mutex_lock_common(struct mutex *lock, long state, unsigned int subclass, struct lockdep_map *nest_lock, unsigned long ip, struct ww_acquire_ctx *ww_ctx, const bool use_ww_ctx) { struct task_struct *task = current; struct mutex_waiter waiter ; unsigned long flags; int ret; if (use_ww_ctx) { struct ww_mutex *ww = container_of(lock, struct ww_mutex, base); if (unlikely(ww_ctx == READ_ONCE(ww->ctx))) return -EALREADY; } preempt_disable(); mutex_acquire_nest(&lock->dep_map, subclass, 0 , nest_lock, ip); if (mutex_optimistic_spin(lock, ww_ctx, use_ww_ctx)) { preempt_enable(); return 0 ; } spin_lock_mutex(&lock->wait_lock, flags); if (!mutex_is_locked(lock) && (atomic_xchg_acquire(&lock->count, 0 ) == 1 )) goto skip_wait; debug_mutex_lock_common(lock, &waiter); debug_mutex_add_waiter(lock, &waiter, task); list_add_tail(&waiter.list , &lock->wait_list); waiter.task = task; lock_contended(&lock->dep_map, ip); for (;;) { if (atomic_read (&lock->count) >= 0 && (atomic_xchg_acquire(&lock->count, -1 ) == 1 )) break ; if (unlikely(signal_pending_state(state, task))) { ret = -EINTR; goto err; } if (use_ww_ctx && ww_ctx->acquired > 0 ) { ret = __ww_mutex_lock_check_stamp(lock, ww_ctx); if (ret) goto err; } __set_task_state(task, state); spin_unlock_mutex(&lock->wait_lock, flags); schedule_preempt_disabled(); spin_lock_mutex(&lock->wait_lock, flags); } __set_task_state(task, TASK_RUNNING); mutex_remove_waiter(lock, &waiter, task); if (likely(list_empty(&lock->wait_list))) atomic_set (&lock->count, 0 ); debug_mutex_free_waiter(&waiter); skip_wait: lock_acquired(&lock->dep_map, ip); mutex_set_owner(lock); if (use_ww_ctx) { struct ww_mutex *ww = container_of(lock, struct ww_mutex, base); ww_mutex_set_context_slowpath(ww, ww_ctx); } spin_unlock_mutex(&lock->wait_lock, flags); preempt_enable(); return 0 ; err: mutex_remove_waiter(lock, &waiter, task); spin_unlock_mutex(&lock->wait_lock, flags); debug_mutex_free_waiter(&waiter); mutex_release(&lock->dep_map, 1 , ip); preempt_enable(); return ret; }
总结:进程在获取 mutex 锁时 count 为 1 时,将 count 减 1 直接获取该锁,访问临界区。如果获取锁时获取不到,则进入如下的休眠流程。
将当前进程放入 mutex 的 wait_list
将 count 设置为 -1 ,表示有进程正在排队
设置进程状态,进程休眠,调度其他进程工作
再来看一下释放流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 void __sched mutex_unlock (struct mutex *lock) { #ifndef CONFIG_DEBUG_MUTEXES mutex_clear_owner(lock); #endif __mutex_fastpath_unlock(&lock->count, __mutex_unlock_slowpath); } EXPORT_SYMBOL(mutex_unlock); static inline void __mutex_fastpath_unlock(atomic_t *count, void (*fail_fn)(atomic_t *)) { if (unlikely(atomic_inc_return_release(count) <= 0 )) fail_fn(count); } __visible void __mutex_unlock_slowpath(atomic_t *lock_count) { struct mutex *lock = container_of(lock_count, struct mutex, count); __mutex_unlock_common_slowpath(lock, 1 ); } __mutex_unlock_common_slowpath(struct mutex *lock, int nested) { unsigned long flags; WAKE_Q(wake_q); if (__mutex_slowpath_needs_to_unlock()) atomic_set (&lock->count, 1 ); spin_lock_mutex(&lock->wait_lock, flags); mutex_release(&lock->dep_map, nested, _RET_IP_); debug_mutex_unlock(lock); if (!list_empty(&lock->wait_list)) { struct mutex_waiter *waiter = list_entry(lock->wait_list.next, struct mutex_waiter, list ); debug_mutex_wake_waiter(lock, waiter); wake_q_add(&wake_q, waiter->task); } spin_unlock_mutex(&lock->wait_lock, flags); wake_up_q(&wake_q); }
总结:如果 count = 0 表示没线程等待,直接将 count 加 1。如果 count 等于 -1 表示有线程等待,进入唤醒流程
从 wait_list 中取出第一个进程,并将其唤醒
根据是否还有进程在等待设置 count 的状态,有进程等待设置为 -1,没有则设置为 0
唤醒的进程从 mutex 链表中删除自己
被唤醒的进程访问临界区资源
常用接口如下
函数接口
接口含义
DEFINE_MUTEX(name);
静态创建化互斥锁 name
mutex_init(&mutex);
动态初始化互斥锁 mutex
mutex_lock(lock);
获取互斥锁,获取不到则进入睡眠
mutex_trylock(lock);
尝试获取互斥锁,成功返回1, 失败返回0
mutex_unlock(lock);
释放互斥锁
mutex_is_lock(lock);
如果锁已被使用返回1,否则返回0
4) 锁的使用场景
在可以睡眠的用户上下文中加锁使用信号量(semaphore) 和互斥锁(mutex) 。其中 mutex 一般不会被长期持有,用于保护一段代码。
在用户上下文与 Softirqs 之间加锁使用 spin_lock_bh/spin_unlock_bh
在用户上下文与 Tasklet 之间加锁 spin_lock_bh/spin_unlock_bh
在用户上下文与 Timer 之间加锁 spin_lock_bh/spin_unlock_bh
不同 cpu 之间在 Tasklet 与 Timer 之间无需加锁
在 Softirq 之间加锁 spin_lock/pin_unlock
三、linux 队列
1、等待队列
等待队列从功能上可以简单理解为:让进程进入睡眠,在你想让他工作的时候唤醒。它有两部分组成首先是挂接我们等待队列项的等待头,等待队列头的数据结构描述如下
1 2 3 4 5 6 7 include/linux/wait.h struct __wait_queue_head { spinlock_t lock; struct list_head task_list ; }; typedef struct __wait_queue_head wait_queue_head_t ;
而挂接在其上面的等待队列项的数据局结构如下
1 2 3 4 5 6 7 8 9 10 11 12 13 typedef struct wait_queue_entry wait_queue_entry_t ;#define WQ_FLAG_EXCLUSIVE 0x01 #define WQ_FLAG_WOKEN 0x02 #define WQ_FLAG_BOOKMARK 0x04 struct wait_queue_entry { unsigned int flags; void *private; wait_queue_func_t func; struct list_head entry ; };
他们的创建与与初始化如下
函数接口
接口含义
DECLARE_WAIT_QUEUE_HEAD(name)
声明等待队列头 name
init_waitqueue_head(name)
初始化等待队列头 name
DECLARE_WAITQUEUE(name, tsk)
定义等待队列成员 name
2. 添加/删除等待队列成员
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 void add_wait_queue (struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry) { unsigned long flags; wq_entry->flags &= ~WQ_FLAG_EXCLUSIVE; spin_lock_irqsave(&wq_head->lock, flags); __add_wait_queue(wq_head, wq_entry); spin_unlock_irqrestore(&wq_head->lock, flags); } EXPORT_SYMBOL(add_wait_queue); static inline void __add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry){ list_add(&wq_entry->entry, &wq_head->head); } void add_wait_queue_exclusive (struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry) { unsigned long flags; wq_entry->flags |= WQ_FLAG_EXCLUSIVE; spin_lock_irqsave(&wq_head->lock, flags); __add_wait_queue_entry_tail(wq_head, wq_entry); spin_unlock_irqrestore(&wq_head->lock, flags); } EXPORT_SYMBOL(add_wait_queue_exclusive); static inline void __add_wait_queue_entry_tail(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry){ list_add_tail(&wq_entry->entry, &wq_head->head); } void remove_wait_queue (struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry) { unsigned long flags; spin_lock_irqsave(&wq_head->lock, flags); __remove_wait_queue(wq_head, wq_entry); spin_unlock_irqrestore(&wq_head->lock, flags); } EXPORT_SYMBOL(remove_wait_queue); static inline void __remove_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry){ list_del(&wq_entry->entry); }
3. 进程休眠
函数接口
接口含义
wait_event(wq, condition)
如果condition为0,则进程休眠,且休眠状态不能被中断打断
wait_event_timeout(wq, condition, timeout)
如果condition为0,则进入休眠,当 timeout 到了则唤醒进程不管此时condition为真为假都会返回
wait_event_interruptible(wq, condition)
如果condition为0,则进程休眠,休眠状态可以被中断打断,当休眠被中断打断时返回0,这时驱动应返回 -ERESTARTSYS
wait_event_interruptible_timeout(wq, condition, timeout)
如果condition为0,则进程休眠,休眠状态可以被中断打断,当休眠被中断打断时返回0,这时驱动应返回 -ERESTARTSY,当 timeout 到了则唤醒进程不管此时condition为真为假都会返回
源码分析如下
1) wait_event
1 2 3 4 5 6 7 8 9 10 #define wait_event(wq_head, condition) do { might_sleep(); if (condition) break ; __wait_event(wq_head, condition); } while (0 ) #define __wait_event(wq_head, condition) \ (void)___wait_event(wq_head, condition, TASK_UNINTERRUPTIBLE, 0, 0, schedule())
2) ___wait_event
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #define ___wait_event(wq_head, condition, state, exclusive, ret, cmd) ({ __label__ __out; struct wait_queue_entry __wq_entry; long __ret = ret; init_wait_entry(&__wq_entry, exclusive ? WQ_FLAG_EXCLUSIVE : 0 ); for (;;) { long __int = prepare_to_wait_event(&wq_head, &__wq_entry, state); if (condition) break ; if (___wait_is_interruptible(state) && __int) { __ret = __int; goto __out; } cmd; } finish_wait(&wq_head, &__wq_entry); __out: __ret; })
3) init_wait_entry
1 2 3 4 5 6 7 8 void init_wait_entry (struct wait_queue_entry *wq_entry, int flags) { wq_entry->flags = flags; wq_entry->private = current; wq_entry->func = autoremove_wake_function; INIT_LIST_HEAD(&wq_entry->entry); } EXPORT_SYMBOL(init_wait_entry);
4). prepare_to_wait_event
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 long prepare_to_wait_event (struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry, int state) { unsigned long flags; long ret = 0 ; spin_lock_irqsave(&wq_head->lock, flags); if (unlikely(signal_pending_state(state, current))) { list_del_init(&wq_entry->entry); ret = -ERESTARTSYS; } else { if (list_empty(&wq_entry->entry)) { if (wq_entry->flags & WQ_FLAG_EXCLUSIVE) __add_wait_queue_entry_tail(wq_head, wq_entry); else __add_wait_queue(wq_head, wq_entry); } set_current_state(state); } spin_unlock_irqrestore(&wq_head->lock, flags); return ret; } EXPORT_SYMBOL(prepare_to_wait_event);
4. 唤醒进程
函数接口
接口含义
void wake_up(wait_queue_head_t *q);
唤醒等待队列 q 上的进程,如果 condition 为真则返回
void wake_up_interruptible(wait_queue_head_t *q);
唤醒等待队列 q 上的进程,如果 condition 为真则返回
源码分析如下
1) __wake_up
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 void __wake_up(struct wait_queue_head *wq_head, unsigned int mode, int nr_exclusive, void *key) { __wake_up_common_lock(wq_head, mode, nr_exclusive, 0 , key); } EXPORT_SYMBOL(__wake_up); static void __wake_up_common_lock(struct wait_queue_head *wq_head, unsigned int mode, int nr_exclusive, int wake_flags, void *key) { unsigned long flags; wait_queue_entry_t bookmark; bookmark.flags = 0 ; bookmark.private = NULL ; bookmark.func = NULL ; INIT_LIST_HEAD(&bookmark.entry); spin_lock_irqsave(&wq_head->lock, flags); nr_exclusive = __wake_up_common(wq_head, mode, nr_exclusive, wake_flags, key, &bookmark); spin_unlock_irqrestore(&wq_head->lock, flags); while (bookmark.flags & WQ_FLAG_BOOKMARK) { spin_lock_irqsave(&wq_head->lock, flags); nr_exclusive = __wake_up_common(wq_head, mode, nr_exclusive, wake_flags, key, &bookmark); spin_unlock_irqrestore(&wq_head->lock, flags); } }
2) __wake_up_common
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 static int __wake_up_common(struct wait_queue_head *wq_head, unsigned int mode, int nr_exclusive, int wake_flags, void *key, wait_queue_entry_t *bookmark) { wait_queue_entry_t *curr, *next; int cnt = 0 ; if (bookmark && (bookmark->flags & WQ_FLAG_BOOKMARK)) { curr = list_next_entry(bookmark, entry); list_del(&bookmark->entry); bookmark->flags = 0 ; } else curr = list_first_entry(&wq_head->head, wait_queue_entry_t , entry); if (&curr->entry == &wq_head->head) return nr_exclusive; list_for_each_entry_safe_from(curr, next, &wq_head->head, entry) { unsigned flags = curr->flags; int ret; if (flags & WQ_FLAG_BOOKMARK) continue ; ret = curr->func(curr, mode, wake_flags, key); if (ret < 0 ) break ; if (ret && (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive) break ; if (bookmark && (++cnt > WAITQUEUE_WALK_BREAK_CNT) && (&next->entry != &wq_head->head)) { bookmark->flags = WQ_FLAG_BOOKMARK; list_add_tail(&bookmark->entry, &next->entry); break ; } } return nr_exclusive; }
2、工作队列
工作队列(work queue)是一种将工作推后执行的形式,它和tasklet有所不同。工作队列可以把工作推后,交由一个内核线程去执行,也就是说,这个下半部分可以在进程上下文中执行。这样,通过工作队列执行的代码能占尽进程上下文的所有优势。最重要的就是工作队列允许被重新调度甚至是睡眠。它的数据结构如下
1 2 3 4 5 6 7 8 9 10 typedef void (*work_func_t ) (struct work_struct *work) ;struct work_struct { atomic_long_t data; struct list_head entry ; work_func_t func; #ifdef CONFIG_LOCKDEP struct lockdep_map lockdep_map ; #endif };
参考Linux内核实践之工作队列
3) 操作函数
1. 初始化工作队列
函数接口
接口含义
INIT_WORK(work,func)
初始化工作队列项 work,并设置回调函数func
DECLARE_WORK(name,func)
声明一个等待队列项 name,并设置回调函数func
2. 调度工作队列
函数接口
接口含义
schedule_work(&work)
调度工作队列
schedule_delayed_work(&work,tick)
延时tick个滴答应答之后调度工作队列
四、linux内核通知链
1、基本概念
内核中不同模块间的消息通知机制,告诉其他模块,当前发生了什么事情。内核通知链只能用于内核之间,不能用于内核于用户空间之间的通信。实现原理很简单,就是通知的模块 提供一个链表,被通知的模块 将回调函数注册进入通知模块提供的链表中。当发生事件时通知模块遍历链表中的所有回调函数,并根据传入的参数依次调用。以原子通知链为例进行说名,原子通知链提供的链表头如下所示。
1 2 3 4 struct atomic_notifier_head { spinlock_t lock; struct notifier_block __rcu *head ; };
无论是什么通知链,都是使用 notifier_block 作为该通知链的成员。
1 2 3 4 5 6 7 8 typedef int (*notifier_fn_t ) (struct notifier_block *nb, unsigned long action, void *data) ;struct notifier_block { notifier_fn_t notifier_call; struct notifier_block __rcu *next ; int priority; };
实现过程很简单,就两步,第一步注册被通知模块的成员 notifier_block 到发出通知模块提供的链表头 atomic_notifier_head 上 ,第二步当发出通知模块要通知时,调用该链表头上的成员的回调函数即可 。注册过程很简单如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 int atomic_notifier_chain_register (struct atomic_notifier_head *nh, struct notifier_block *n) { unsigned long flags; int ret; spin_lock_irqsave(&nh->lock, flags); ret = notifier_chain_register(&nh->head, n); spin_unlock_irqrestore(&nh->lock, flags); return ret; } EXPORT_SYMBOL_GPL(atomic_notifier_chain_register); static int notifier_chain_register (struct notifier_block **nl, struct notifier_block *n) { while ((*nl) != NULL ) { if (n->priority > (*nl)->priority) break ; nl = &((*nl)->next); } n->next = *nl; rcu_assign_pointer(*nl, n); return 0 ; }
再来看一下调用过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 int atomic_notifier_call_chain (struct atomic_notifier_head *nh, unsigned long val, void *v) { return __atomic_notifier_call_chain(nh, val, v, -1 , NULL ); } EXPORT_SYMBOL_GPL(atomic_notifier_call_chain); NOKPROBE_SYMBOL(atomic_notifier_call_chain); int __atomic_notifier_call_chain(struct atomic_notifier_head *nh, unsigned long val, void *v, int nr_to_call, int *nr_calls) { int ret; rcu_read_lock(); ret = notifier_call_chain(&nh->head, val, v, nr_to_call, nr_calls); rcu_read_unlock(); return ret; } EXPORT_SYMBOL_GPL(__atomic_notifier_call_chain); NOKPROBE_SYMBOL(__atomic_notifier_call_chain); static int notifier_call_chain (struct notifier_block **nl, unsigned long val, void *v, int nr_to_call, int *nr_calls) { int ret = NOTIFY_DONE; struct notifier_block *nb , *next_nb ; nb = rcu_dereference_raw(*nl); while (nb && nr_to_call) { next_nb = rcu_dereference_raw(nb->next); #ifdef CONFIG_DEBUG_NOTIFIERS if (unlikely(!func_ptr_is_kernel_text(nb->notifier_call))) { WARN(1 , "Invalid notifier called!" ); nb = next_nb; continue ; } #endif ret = nb->notifier_call(nb, val, v); if (nr_calls) (*nr_calls)++; if (ret & NOTIFY_STOP_MASK) break ; nb = next_nb; nr_to_call--; } return ret; } NOKPROBE_SYMBOL(notifier_call_chain);
2、接口整理
1、原子通知链
可以看出原子通知链使用的是自旋锁,因此原子通知链可以用在中断上下文中,不能阻塞。
1 2 3 4 struct atomic_notifier_head { spinlock_t lock; struct notifier_block __rcu *head ; };
创建原子通知连
1 2 3 4 5 6 7 #define ATOMIC_NOTIFIER_HEAD(name) \ struct atomic_notifier_head name = \ ATOMIC_NOTIFIER_INIT(name) #define ATOMIC_NOTIFIER_INIT(name) { \ .lock = __SPIN_LOCK_UNLOCKED(name.lock), \ .head = NULL }
调用原子通知链
1 2 3 4 5 6 7 8 9 10 11 12 13 int atomic_notifier_call_chain (struct atomic_notifier_head *nh, unsigned long val, void *v) ;int __atomic_notifier_call_chain(struct atomic_notifier_head *nh, unsigned long val, void *v, int nr_to_call, int *nr_calls);
注册原子通知链
1 2 extern int atomic_notifier_chain_register (struct atomic_notifier_head *nh, struct notifier_block *nb)
2、阻塞通知链
阻塞通知连使用的读写信号量,因此阻塞通知链使用在进程上下文,可以阻塞。
1 2 3 4 struct blocking_notifier_head { struct rw_semaphore rwsem ; struct notifier_block __rcu *head ; };
创建阻塞通知链
1 2 3 4 5 6 7 #define BLOCKING_NOTIFIER_HEAD(name) \ struct blocking_notifier_head name = \ BLOCKING_NOTIFIER_INIT(name) #define BLOCKING_NOTIFIER_INIT(name) { \ .rwsem = __RWSEM_INITIALIZER((name).rwsem), \ .head = NULL }
调用阻塞通知链
1 2 3 4 5 6 7 8 9 10 11 12 int blocking_notifier_call_chain (struct blocking_notifier_head *nh, unsigned long val, void *v) ;int __blocking_notifier_call_chain(struct blocking_notifier_head *nh, unsigned long val, void *v, int nr_to_call, int *nr_calls);
注册阻塞通知链
1 extern int blocking_notifier_chain_register (struct blocking_notifier_head *nh,struct notifier_block *nb) ;
3、原始通知链
非常原始,啥锁都没有
1 2 3 struct raw_notifier_head { struct notifier_block *head ; };
创建原始通知链
1 2 3 4 5 6 #define RAW_NOTIFIER_HEAD(name) \ struct raw_notifier_head name = \ RAW_NOTIFIER_INIT(name) #define RAW_NOTIFIER_INIT(name) { \ .head = NULL }
调用原始通知链
1 2 3 4 5 6 7 8 9 10 11 12 13 int raw_notifier_call_chain (struct raw_notifier_head *nh, unsigned long val, void *v) ;int __raw_notifier_call_chain(struct raw_notifier_head *nh, unsigned long val, void *v, int nr_to_call, int *nr_calls);
注册原始通链
1 int raw_notifier_chain_register (struct raw_notifier_head *nh, struct notifier_block *nb) ;
4、SRCU 通知链
可以阻塞的通知链。
1 2 3 4 5 struct srcu_notifier_head { struct mutex mutex ; struct srcu_struct srcu ; struct notifier_block *head ; };
五、内核定时器
内核定时器比较简单只给出示例程序