Linux内核的同步/互斥机制(待完善)

本文深入探讨了Linux内核中的同步机制,包括自旋锁、信号量和读写锁。自旋锁适合保护短时的临界区,不会引起调用者睡眠,而信号量和读写信号量允许持有者睡眠,适合保护可能长时间执行的任务。读写锁进一步区分了读写操作,允许多个读者同时访问,但写者与其他所有读者和写者互斥。此外,文章还提到了实时互斥量、原子操作、自旋锁和读写自旋锁等技术,以适应不同场景下的同步需求。
摘要由CSDN通过智能技术生成


Linux是一个多用户多任务的操作系统,在多处理器(SMP)情况下,存在真正的并行运算。
内核同步机制和用户空间的同步机制并不是一 一对应的,但是基本的思想都是相同的:保护临界区。只是内核同步机制更适合于在解决内核中的同步问题。

在内核中,可能出现多个进程(通过系统调用进入内核模式)访问同一个对象、进程和硬中断访问同一个对象、进程和软中断访问同一个对象、多个处理器访问同一个对象等现象,我们需要使用互斥技术,确保在给定的时刻只有一个主体可以进入临界区访问对象。

自旋锁与信号量的第一个区别是前者不会引起调用者睡眠。自旋锁与信号量的选用应该取决于锁被持有的时间长短。如果锁的持有时间较短, 使用自旋锁是更好的选择。自旋锁与信号量的第二个区别是信号量允许有多个持有者, 而自旋锁只能有一个持有者。

情况使用的互斥方法
如果临界区的执行时间比较长或者可能睡眠
(1)信号量,大多数情况下我们使用互斥信号量。
(2)读写信号量。
(3)互斥锁。
(4)实时互斥锁。
注意:申请这些锁的时候,如果锁被其他进程占有,进程将会睡眠等待,代价很高。
如果临界区的执行时间很短,并且不会睡眠,那么使用上面的锁不太合适,因为进程切换的代价很高,可以使用下面这些互斥技术。
(1)原子变量。
(2)自旋锁。
(3)读写自旋锁,它是对自旋锁的改进,允许多个读者同时进入临界区。
(4)顺序锁,它是对读写自旋锁的改进,读者不会阻塞写者。
注意:申请这些锁的时候,如果锁被其他进程占有,进程自旋等待(也称为忙等待)。
进程还可以使用下面的互斥技术。
(1)禁止内核抢占,防止被当前处理器上的其他进程抢占,实现和当前处理器上的其他进程互斥。
(2)禁止软中断,防止被当前处理器上的软中断抢占,实现和当前处理器上的软中断互斥。
(3)禁止硬中断,防止被当前处理器上的硬中断抢占,实现和当前处理器上的硬中断互斥。
在多处理器系统中,为了提高程序的性能,需要尽量减少处理器之间的互斥,使处理器可以最大限度地并行执行。从互斥信号量到读写信号量的改进,从自旋锁到读写自旋锁的改进,允许读者并行访问临界区,提高了并行性能,但是我们还可以进一步提高并行性能,使用下面这些避免使用锁的互斥技术。
(1)每处理器变量。
(2)每处理器计数器。
(3)内存屏障。
(4)读-复制更新(Read-Copy Update,RCU)。
(5)可睡眠RCU。

使用锁保护临界区,如果使用不当,可能出现死锁问题。内核里面的锁非常多,定位
很难,为了方便定位死锁问题,内核提供了死锁检测工具lockdep。

一、临界区的执行时间比较长或者可能睡眠

1.1 信号量(semaphore)

1.1.1 概述

信号量允许多个进程同时进入临界区。信号量是一种睡眠锁。假如有一个任务想要获得己被占用的信号量, 信号量就会将其放入一个等待队列然后让其睡眠, 这样CPU 可以去处理其他事情。持有信号量的进程将信号释放后, 处于等待队列中的一个任务将被唤醒并获得信号量。
和自旋锁相比,信号量适合保护比较长的临界区,因为竞争信号量时进程可能睡眠和
再次唤醒,代价很高。

1.1.2 使用方法

内核使用的信号量定义如下。
路径:include/linux/semaphore.h

struct semaphore {
	raw_spinlock_t lock;
	unsigned int count;
	struct list_head wait_list;
};

成员lock 是自旋锁,用来保护信号量的其他成员。
成员count 是计数值,表示还可以允许多少个进程进入临界区。
成员wait_list 是等待进入临界区的进程链表。
注意不要直接访问该结构体的成员。
初始化静态信号量的方法如下:

1__SEMAPHORE_INITIALIZER(name, n)//指定名称和计数值,允许n 个进程同时进入临界区。2DEFINE_SEMAPHORE(name)//初始化一个互斥信号量。

在运行时动态初始化信号量的方法如下:

static inline void sema_init(struct semaphore *sem, int val);

参数val 指定允许同时进入临界区的进程数量。
获取信号量的函数如下:

序号函数说明
(1)void down(struct semaphore *sem);获取信号量,如果计数值是0,进程深度睡眠。down 函数会一直等待信号量。
(2)int down_interruptible(struct semaphore *sem);获取信号量,如果计数值是0,进程轻度睡眠。能被信号打断, 它的返回值如果是0, 表示获得信号量正常返回; 如果是-EINTR, 表示被信号打断。
(3)int down_killable(struct semaphore *sem);获取信号量,如果计数值是0,进程中度睡眠。
(4)int down_trylock(struct semaphore *sem);获取信号量,如果计数值是0,进程不等待。(即信号量被占用则立即返回)
(5)int down_timeout(struct semaphore *sem, long jiffies);获取信号量,指定等待的时间。

计数值是0表示信号量被占用。
down 函数会导致睡眠, 因此不能在中断上下文中使用。在中断上下文中应该选用down_trylock函数。
释放信号量的函数如下:

void up(struct semaphore *sem);

1.1.3 实例

本例使用信号量实现读写互斥, 在写过程中特意增加了写延迟, 以方便观察代码运行结果。

//设备结构
struct DEMO_dev 
{
	struct semaphore sem;     /* mutual exclusion semaphore     */
	struct cdev cdev;	  /* Char device structure		*/
};

ssize_t DEMO_read(struct file *filp, char __user *buf, size_t count,loff_t *f_pos)
{
	struct DEMO_dev *dev = filp->private_data; 
	if (down_interruptible(&dev->sem))
		return -ERESTARTSYS;
	
    /* 把数据拷贝到应用程序空间 */
	if (copy_to_user(buf,demoBuffer,count))
	{
	   count=-EFAULT; 
	}
	up(&dev->sem);
	return count;
}

ssize_t DEMO_write(struct file *filp, const char __user *buf, size_t count,loff_t *f_pos)
{
	struct DEMO_dev *dev = filp->private_data;
	ssize_t retval = -ENOMEM; /* value used in "goto out" statements */

	if (down_interruptible(&dev->sem))
	{
		return -ERESTARTSYS;
	}
	/* 把数据拷贝到内核空间 */
	if (copy_from_user(demoBuffer+*f_pos, buf, count))
	{
		count = -EFAULT;
	}
	printk("write delay\n");
	msleep(1000*10);
	printk("write delay ok\n");
	up(&dev->sem);
	return count;
}
int DEMO_init_module(void)
{
	sema_init(&DEMO_devices->sem,1);
}

1.2 读写信号量(rw_semaphore)

1.2.1 概述

读写信号量是对互斥信号量的改进,它区分读写操作,允许多个读者同时进入临界区,读者和写者互斥,写者和写者互斥,适合在以读为主的情况使用。它的原理与读写锁差不多。

1.2.2 使用方法

读写信号量的定义如下:
路径:include/linux/rwsem.h

struct rw_semaphore {
	atomic_long_t count;
	struct list_head wait_list;
	raw_spinlock_t wait_lock;
	struct task_struct *owner;};

初始化静态读写信号量的方法如下:

DECLARE_RWSEM(name);

在运行时动态初始化读写信号量的方法如下:

init_rwsem(sem);

申请读锁的函数如下:

序号函数说明
(1)void down_read(struct rw_semaphore *sem);申请读锁,如果写者占有写锁或者正在等待写锁,那么进程深度睡眠。
(2)int down_read_trylock(struct rw_semaphore *sem);尝试申请读锁,不会等待。如果申请成功,返回1;否则返回0。

释放读锁的函数如下:

void up_read(struct rw_semaphore *sem);

申请写锁的函数如下:

序号函数说明
(1)void down_write(struct rw_semaphore *sem);申请写锁,如果写者占有写锁或者读者占有读锁,那么进程深度睡眠。
(2)int down_write_killable(struct rw_semaphore *sem);申请写锁,如果写者占有写锁或者读者占有读锁,那么进程中度睡眠。
(3)int down_write_trylock(struct rw_semaphore *sem);尝试申请写锁,不会等待。如果申请成功,返回1;否则返回0。

占有写锁以后,可以把写锁降级为读锁,函数如下:

void downgrade_write(struct rw_semaphore *sem);

释放写锁的函数如下:

void up_write(struct rw_semaphore *sem);

1.2.3 实例

路径:kernel-4.9/drivers/cpufreq/cpufreq.c

struct cpufreq_policy {
	.........
	struct rw_semaphore	rwsem;
	.........
};

static ssize_t show(struct kobject *kobj, struct attribute *attr, char *buf)
{
	struct cpufreq_policy *policy = to_policy(kobj);
	struct freq_attr *fattr = to_attr(attr);
	ssize_t ret;

	if (!fattr->show)
		return -EIO;

	down_read(&policy->rwsem);
	ret = fattr->show(policy, buf);
	up_read(&policy->rwsem);

	return ret;
}
static ssize_t store(struct kobject *kobj, struct attribute *attr,
		     const char *buf, size_t count)
{
	struct cpufreq_policy *policy = to_policy(kobj);
	struct freq_attr *fattr = to_attr(attr);
	ssize_t ret = -EINVAL;

	if (!fattr->store)
		return -EIO;

	get_online_cpus();

	if (cpu_online(policy->cpu)) {
		down_write(&policy->rwsem);
		ret = fattr->store(policy, buf, count);
		up_write(&policy->rwsem);
	}

	put_online_cpus();

	return ret;
}
static struct cpufreq_policy *cpufreq_policy_alloc(unsigned int cpu)
{
	struct cpufreq_policy *policy;

	policy = kzalloc(sizeof(*policy), GFP_KERNEL);
	if (!policy)
		return NULL;
	init_rwsem(&policy->rwsem);
}

1.3 互斥量

1.3.1 概述

有些书把互斥量也叫互斥锁。
互斥量只允许一个进程进入临界区,适合保护时间比较长的临界区。互斥量加锁失败会进入睡眠,等待唤醒, 不能用于中断上下文。因为竞争互斥量时进程可能睡眠和再次唤醒,代价很高。
尽管可以把二值信号量(即把信号量的计数值设置为1)当作互斥锁使用,但是内核单独实现了互斥锁。

1.3.2 使用方法

互斥锁的定义如下:
路径:include/linux/mutex.h

struct mutex {
	atomic_long_t owner;
	spinlock_t wait_lock;
	#ifdef CONFIG_MUTEX_SPIN_ON_OWNER
	struct optimistic_spin_queue osq;
	#endif
	struct list_head wait_list;};

初始化静态互斥锁的方法如下:

DEFINE_MUTEX(mutexname);

在运行时动态初始化互斥锁的方法如下:

mutex_init(mutex);

申请互斥锁的函数如下:

序号函数说明
(1)void mutex_lock(struct mutex *lock);申请互斥锁,如果锁被占有,进程深度睡眠。
(2)int mutex_lock_interruptible(struct mutex *lock);申请互斥锁,如果锁被占有,进程轻度睡眠。
(3)int mutex_lock_killable(struct mutex *lock);申请互斥锁,如果锁被占有,进程中度睡眠。
(4)int mutex_trylock(struct mutex *lock);申请互斥锁,如果申请成功,返回1;如果锁被其他进程占有,那么进程不等待,返回0。

释放互斥锁的函数如下:

void mutex_unlock(struct mutex *lock);

1.3.3 实例

路径:kernel-4.9/sound/soc/codecs/rt5677.c

struct rt5677_priv {
	.......
	struct mutex dsp_cmd_lock, dsp_pri_lock;
};

static int rt5677_read(void *context, unsigned int reg, unsigned int *val)
{
	struct i2c_client *client = context;
	struct rt5677_priv *rt5677 = i2c_get_clientdata(client);

	if (rt5677->is_dsp_mode) {
		if (reg > 0xff) {
			mutex_lock(&rt5677->dsp_pri_lock);
			rt5677_dsp_mode_i2c_write(rt5677, RT5677_PRIV_INDEX,
				reg & 0xff);
			rt5677_dsp_mode_i2c_read(rt5677, RT5677_PRIV_DATA, val);
			mutex_unlock(&rt5677->dsp_pri_lock);
		} else {
			rt5677_dsp_mode_i2c_read(rt5677, reg, val);
		}
	} else {
		regmap_read(rt5677->regmap_physical, reg, val);
	}

	return 0;
}

static int rt5677_write(void *context, unsigned int reg, unsigned int val)
{
	struct i2c_client *client = context;
	struct rt5677_priv *rt5677 = i2c_get_clientdata(client);

	if (rt5677->is_dsp_mode) {
		if (reg > 0xff) {
			mutex_lock(&rt5677->dsp_pri_lock);
			rt5677_dsp_mode_i2c_write(rt5677, RT5677_PRIV_INDEX,
				reg & 0xff);
			rt5677_dsp_mode_i2c_write(rt5677, RT5677_PRIV_DATA,
				val);
			mutex_unlock(&rt5677->dsp_pri_lock);
		} else {
			rt5677_dsp_mode_i2c_write(rt5677, reg, val);
		}
	} else {
		regmap_write(rt5677->regmap_physical, reg, val);
	}

	return 0;
}

static int rt5677_probe(struct snd_soc_codec *codec)
{
	struct snd_soc_dapm_context *dapm = snd_soc_codec_get_dapm(codec);
	struct rt5677_priv *rt5677 = snd_soc_codec_get_drvdata(codec);

	mutex_init(&rt5677->dsp_pri_lock);
}

1.4 实时互斥量

1.4.1 概述

实时互斥量是对互斥量的改进,实现了优先级继承(priority inheritance),解决了优先
级反转(priority inversion)问题。
什么是优先级反转问题?
假设进程1 的优先级低,进程2 的优先级高。进程1 持有互斥量,进程2 申请互斥量,
因为进程1 已经占有互斥量,所以进程2 必须睡眠等待,导致优先级高的进程2 等待优先
级低的进程1。
如果存在进程3,优先级在进程1 和进程2 之间,情况更糟糕。假设进程1 仍然持有
互斥量,进程2 正在等待。进程3 开始运行,因为它的优先级比进程1 高,所以它可以抢
占进程1,导致进程1 持有互斥量的时间延长,进程2 等待的时间延长。
优先级继承可以解决优先级反转问题。如果低优先级的进程持有互斥量,高优先级的
进程申请互斥量,那么把持有互斥量的进程的优先级临时提升到申请互斥量的进程的优先
级。在上面的例子中,把进程1 的优先级临时提升到进程2 的优先级,防止进程3 抢占进
程1,使进程1 尽快执行完临界区,减少进程2 的等待时间。
如果需要使用实时互斥量,编译内核时需要开启配置宏CONFIG_RT_MUTEXES。

1.4.2 使用方法

实时互斥量的定义如下:
路径:include/linux/rtmutex.h

struct rt_mutex {
	raw_spinlock_t wait_lock;
	struct rb_root waiters;
	struct rb_node *waiters_leftmost;
	struct task_struct *owner;};

初始化静态实时互斥量的方法如下:

DEFINE_RT_MUTEX(mutexname);

在运行时动态初始化实时互斥量的方法如下:

rt_mutex_init(mutex);

申请实时互斥量的函数如下:

序号函数说明
(1)void rt_mutex_lock(struct rt_mutex *lock);申请实时互斥量,如果量被占有,进程深度睡眠。
(2)int rt_mutex_lock_interruptible(struct rt_mutex *lock);申请实时互斥量,如果量被占有,进程轻度睡眠。
(3)int rt_mutex_timed_lock(struct rt_mutex *lock, struct hrtimer_sleeper *timeout);申请实时互斥量,如果量被占有,进程睡眠等待一段时间。
(4)int rt_mutex_trylock(struct rt_mutex *lock);申请实时互斥量,如果申请成功,返回1;如果量被其他进程占有,进程不等待,返回0。

释放实时互斥量的函数如下:

void rt_mutex_unlock(struct rt_mutex *lock);

1.4.3 实例

路径:kernel-4.9/drivers/i2c/i2c-core.c
在i2c读写数据必然用到的函数 i2c_transfer 中,查看实时互斥量的使用。
调用关系:i2c_transfer-> i2c_trylock_bus->trylock_bus->i2c_adapter_trylock_bus->rt_mutex_trylock
i2c_lock_bus和i2c_unlock_bus类似。
这里利用实时互斥量,确保了在某一时刻只能有一个应用或驱动通过调用i2c_transfer进行读写I2C。
当处于原子操作或硬中断被禁止的情况下,调用i2c_trylock_bus,否则调用i2c_lock_bus。

static void i2c_adapter_lock_bus(struct i2c_adapter *adapter,
				 unsigned int flags)
{
	rt_mutex_lock(&adapter->bus_lock);
}

static int i2c_adapter_trylock_bus(struct i2c_adapter *adapter,
				   unsigned int flags)
{
	return rt_mutex_trylock(&adapter->bus_lock);
}

static void i2c_adapter_unlock_bus(struct i2c_adapter *adapter,
				   unsigned int flags)
{
	rt_mutex_unlock(&adapter->bus_lock);
}

static const struct i2c_lock_operations i2c_adapter_lock_ops = {
	.lock_bus =    i2c_adapter_lock_bus,
	.trylock_bus = i2c_adapter_trylock_bus,
	.unlock_bus =  i2c_adapter_unlock_bus,
};

static inline void
i2c_lock_bus(struct i2c_adapter *adapter, unsigned int flags)
{
	adapter->lock_ops->lock_bus(adapter, flags);
}

static inline int
i2c_trylock_bus(struct i2c_adapter *adapter, unsigned int flags)
{
	return adapter->lock_ops->trylock_bus(adapter, flags);
}

static inline void
i2c_unlock_bus(struct i2c_adapter *adapter, unsigned int flags)
{
	adapter->lock_ops->unlock_bus(adapter, flags);
}

int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
{
	int ret;
	
	if (adap->algo->master_xfer) {
#ifdef DEBUG
		for (ret = 0; ret < num; ret++) {
			dev_dbg(&adap->dev,
				"master_xfer[%d] %c, addr=0x%02x, len=%d%s\n",
				ret, (msgs[ret].flags & I2C_M_RD) ? 'R' : 'W',
				msgs[ret].addr, msgs[ret].len,
				(msgs[ret].flags & I2C_M_RECV_LEN) ? "+" : "");
		}
#endif

		if (in_atomic() || irqs_disabled()) {
			ret = i2c_trylock_bus(adap, I2C_LOCK_SEGMENT);
			if (!ret)
				/* I2C activity is ongoing. */
				return -EAGAIN;
		} else {
			i2c_lock_bus(adap, I2C_LOCK_SEGMENT);
		}

		ret = __i2c_transfer(adap, msgs, num);
		i2c_unlock_bus(adap, I2C_LOCK_SEGMENT);

		return ret;
	} else {
		dev_dbg(&adap->dev, "I2C level transfers not supported\n");
		return -EOPNOTSUPP;
	}
}
EXPORT_SYMBOL(i2c_transfer);

二、临界区的执行时间很短,并且不会睡眠

2.1 原子操作(即原子变量)

原子,是最小的可以独立存在的物质单元,是不可分割。把一个完整的,不可中断的操作过程称为原子操作。Linux系统中的原子本质就是对一个int类型变量进行操作,但是,它整个操作操作进行控制,保证这个过程是一次性完成。

原子变量用来实现对整数的互斥访问,通常用来实现计数器。
在对称多处理器结构中,即使能在单条指令中完成的操作也有可能被打断。原子性不可能由软件单独保证,必须有硬件的支持,因此是和平台相关的,而且通常使用汇编语言实现。
原子变量是由编译器来保证的,保证一个线程对数据的操作不会被其他线程打断。
原子变量需要各种处理器架构提供特殊的指令支持,ARM64处理器提供了如下指令:
(1)独占加载指令 ldxr
(2)独占存储指令 stxr
我们不用关心具体的ldxr和stxr,只需要知道怎么使用原子变量atomic_t就可以了。

2.1.1 概述

场景一:
例如,我们写一行代码把变量a加1,编译器把代码编译成3条汇编指令。
(1)把变量a从内存加载到寄存器。
(2)把寄存器的值加1。
(3)把寄存器的值写回内存。

如果这个操作序列是串行化的操作(在一个thread中串行执行),那么一切OK,然而,世界总是不能如你所愿。在多CPU体系结构中,运行在两个CPU上的两个内核控制路径同时并行执行上面操作序列,有可能发生下面的场景:

CPU1上的操作CPU2上的操作
读操作
读操作
修改修改
写操作
写操作

多个CPUs和memory chip是通过总线互联的,在任意时刻,只能有一个总线master设备(例如CPU、DMA controller)访问该Slave设备(在这个场景中,slave设备是RAM chip)。因此,来自两个CPU上的读memory操作被串行化执行,分别获得了同样的旧值。完成修改后,两个CPU都想进行写操作,把修改的值写回到memory。但是,硬件arbiter的限制使得CPU的写回必须是串行化的,因此CPU1首先获得了访问权,进行写回动作,随后,CPU2完成写回动作。在这种情况下,CPU1的对memory的修改被CPU2的操作覆盖了,因此执行结果是错误的。

不仅是多CPU,在单CPU上也会由于有多个内核控制路径的交错而导致上面描述的错误。一个具体的例子如下:

系统调用的控制路径中断handler控制路径
读操作
读操作
修改
写操作
修改
写操作

系统调用的控制路径上,完成读操作后,硬件触发中断,开始执行中断handler。这种场景下,中断handler控制路径的写回的操作被系统调用控制路径上的写回覆盖了,结果也是错误的。
场景二:
驱动开发常见的一个问题:一个驱动可以被多个进程同时打开、使用,这样会导致驱动功能混乱。
./app & ./app & ./app 可以同时打开这个设备,可通过文件描述符调用驱动的其他接口操作这个设备。

如果只想一个进程使用这个设备,只需要在open接口函数做打开判断。

示例:

static int  open_flag = 0;
//打开设备时候执行的程序
static int  chrdev_open(struct inode *pinode, struct file *pfile)
{
   //打开判断
    if(open_flag)
        return -EBUSY;
    open_flag = 1;   //设置    设备已经被打开的标志
     
    printk(KERN_EMERG "line:%d,%s is call\n", __LINE__, __FUNCTION__);

    return 0;
}

//关闭设备时执行的程序
static int chrdev_release(struct inode *pinode, struct file *pfile)
{
    open_flag = 0;   //设置    设备已经被打开的标志
    printk(KERN_EMERG "line:%d,%s is call\n", __LINE__, __FUNCTION__);
    return 0;
}

以上的效果实现,但是不安全。
分析:
open_flag = 1; 这条语句编译汇编 最少得3条。 只需要执行时候没有一次完成,中途被其他进程打开,刚刚这个进程又调用open打开这个设备。这样,新进程也可以打开成功。

怎么样才可以安全做一次性执行完成这三个步骤?

关系统中断; -->关中断后不会任务调度情况,这样就是安全。
open_flag = 1;
开系统中断; -->重新恢复系统调度

示例:
unsigned long flags;
raw_local_irq_save(flags); //关中断
open_flag = 1; //设置 设备已经被打开的标志
raw_local_irq_restore(flags); //开中断

Linux内核已经提供相关的一些,很方便实现这样的效果。原子操作,信号量,互斥信号量,自旋锁。

2.1.2 对策

对于那些有多个内核控制路径进行read-modify-write的变量,内核提供了一个特殊的类型atomic_t,具体定义如下:

(1) 整数原子变量
  路径: include/linux/types.h
  
typedef struct {
	int counter;
} atomic_t;2)长整数原子变量,数据类型是atomic_long_t。
  路径:include/asm-generic/atomic-long.h
  
typedef atomic_t atomic_long_t;
atomic_long_t 实际上就是 atomic_t 类型
(364位整数原子变量
  路径: include/linux/types.h
  
#ifdef CONFIG_64BIT
typedef struct {
	long counter;
} atomic64_t;
#endif

从上面的定义来看,atomic_t实际上就是一个int类型的counter,不过定义这样特殊的类型atomic_t是有其思考的:内核定义了若干atomic_xxx的接口API函数,这些函数只会接收atomic_t类型的参数。这样可以确保atomic_xxx的接口函数只会操作atomic_t类型的数据。同样的,如果你定义了atomic_t类型的变量(你期望用atomic_xxx的接口API函数操作它),这些变量也不会被那些普通的、非原子变量操作的API函数接受。

具体的接口API函数整理如下:
atomic.h (include\linux)
atomic.h (include\asm-generic)

接口函数描述
ATOMIC_INIT(i)初始化原子变量为 i。 #define ATOMIC_INIT(i) { (i) }
static inline void atomic_add(int i, atomic_t *v)给一个原子变量v增加i
static inline int atomic_add_return(int i, atomic_t *v)同上,只不过将变量v的最新值返回
static inline void atomic_sub(int i, atomic_t *v)给一个原子变量v减去i
static inline int atomic_sub_return(int i, atomic_t *v)同上,只不过将变量v的最新值返回
static inline int atomic_cmpxchg(atomic_t *ptr, int old, int new)比较old和原子变量ptr中的值,如果相等,那么就把new值赋给原子变量。返回旧的原子变量ptr中的值
atomic_read(atomic_t * v)获取原子变量的值
atomic_set(atomic_t * v, int i)设定原子变量的值
atomic_inc(v)原子变量的值加一
atomic_inc_return(v)同上,只不过将变量v的最新值返回
atomic_dec(v)原子变量的值减去一
atomic_dec_return(v)同上,只不过将变量v的最新值返回
atomic_dec_and_test(v)对原子类型的变量 v 原子减 1,并判断结果是否为 0,如果为 0,返回真,否则返回假。
atomic_sub_and_test(i, v)给一个原子变量v减去i,并判断变量v的最新值是否等于0
atomic_add_negative(i,v)给一个原子变量v增加i,并判断变量v的最新值是否是负数
static inline int atomic_add_unless(atomic_t *v, int a, int u)只要原子变量v不等于u,那么就执行原子变量v加a的操作。

如果v不等于u,返回非0值,否则返回0值

2.1.3 实例

(1)为了说明原子操作的作用和用法,现在举个例子,使用原子变量实现设备只能被一个进程打开。

 #include <linux/module.h> /* Needed by all modules */
#include <linux/init.h> /* Needed for the module-macros */
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <asm/atomic.h>
#define LEDS_MAJOR 255
#define DEVICE_NAME "miscdev_atomic"
static atomic_t miscdev_atomic = ATOMIC_INIT(1); //定义并初始化一个原子变量
static int first_miscdev_open(struct inode *pinode, struct file *pfile)
{
	printk (KERN_EMERG "Linux miscdevice:%s is call\r\n",__FUNCTION__);
	if( !atomic_dec_and_test(&miscdev_atomic) ) {
		printk(KERN_ERR DEVICE_NAME " is already open!\n");
		atomic_inc(&miscdev_atomic);
		return -EBUSY;
	}
	return 0;
}
int first_miscdev_release (struct inode *inode, struct file *file)
{
	printk (KERN_EMERG "Linux miscdevice:%s is call\r\n",__FUNCTION__);
	atomic_inc(&miscdev_atomic); //释放原子变量
	return 0;
}
static struct file_operations dev_fops =
{
	.owner = THIS_MODULE,
	.open = first_miscdev_open,
	.release= first_miscdev_release,
};
static struct miscdevice misc =
{
	.minor = LEDS_MAJOR,
	.name = DEVICE_NAME,
	.fops = &dev_fops,
};
/* 模块装载执行函数 */
static int __init first_miscdev_init(void)
{
	int ret;
	ret = misc_register(&misc); //注册混杂设备
	if(ret <0)
	printk (KERN_EMERG DEVICE_NAME"\t err\r\n");
	printk (KERN_EMERG DEVICE_NAME"\tinitialized\n");
	return ret;
}
/* 模块卸载执行函数 */
static void __exit first_miscdev_exit(void)
{
	misc_deregister(&misc);
	printk(KERN_EMERG "Goodbye,cruel world!, priority = 0\n");
}
module_init(first_miscdev_init);
module_exit(first_miscdev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("XYD");
MODULE_DESCRIPTION("This the samlpe drv_test");

从上面可以看到,其实只是在原来的字符设备驱动的 open 函数中增加了原子操作判断相关的代码,在关闭函数中增了释放原子操作的代码。 其余的代码基本不用改动,这样就可以保证这个驱动只能被一个进程使用,保证了驱动的安全性。当然,原子操作代码不仅仅是可以放在 open 函数上,可以根据需要放在不同的地方,比如放在 write 函数中,可以实现驱动可以被多进程打开,但是在同时刻只能被一个进程写操作,读操作可以被多进程读,因为读操作不会破坏数据, 而写操作会, 所以, 有时候这处功能恰好是我们想要的结果,就可以这样写。原子操作提供了很多 API,以上只以使用其中一种写法,读者可以在理解了的基础上使用其他 API 来实现相同的功能。
(2)
实际使用代码,路径:kernel-4.9\drivers\misc\panel.c


static atomic_t lcd_available = ATOMIC_INIT(1);
static int lcd_open(struct inode *inode, struct file *file)
{
	int ret;

	ret = -EBUSY;
	if (!atomic_dec_and_test(&lcd_available))
		goto fail; /* open only once at a time */

	ret = -EPERM;
	if (file->f_mode & FMODE_READ)	/* device is write-only */
		goto fail;

	if (lcd.must_clear) {
		lcd_clear_display();
		lcd.must_clear = false;
	}
	return nonseekable_open(inode, file);

 fail:
	atomic_inc(&lcd_available);
	return ret;
}

static int lcd_release(struct inode *inode, struct file *file)
{
	atomic_inc(&lcd_available);
	return 0;
}

static const struct file_operations lcd_fops = {
	.write   = lcd_write,
	.open    = lcd_open,
	.release = lcd_release,
	.llseek  = no_llseek,
};

2.2 自旋锁

2.2.1 概念

自旋的意思就是一直循环直到条件满足。自旋锁不会引起调用者睡眠。如果不能在很短的时间内获得锁, 这无疑会导致CPU 效率降低。
自旋锁用于处理器之间的互斥,适合保护很短的临界区,并且不允许在临界区睡眠。申请自旋锁的时候,如果自旋锁被其他处理器占有, 本处理器自旋等待(也称为忙等待)。
进程、软中断和硬中断都可以使用自旋锁。

自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,“自旋”一词就是因此而得名。自旋锁在内核中大量应用于中断处理等部分(对于单处理器来说,防止中断处理中的并发可简单采用关闭中断的方式,即在标志寄存器中关闭/打开中断标志位,不需要自旋锁)。
自旋锁的初衷就是:在短期间内进行轻量级的锁定。一个被争用的自旋锁使得请求它的线程在等待锁重新可用的期间进行自旋(特别浪费处理器时间),所以自旋锁不应该被持有时间过长。如果需要长时间锁定的话, 最好使用信号量。
自旋锁只有在内核可抢占或SMP(多处理器)的情况下才真正需要,在单CPU且不可抢占的内核下,自旋锁的所有操作都是空操作。

目前内核的自旋锁是排队自旋锁,算法类似于银行柜台的排队叫号。
(1)锁拥有排队号和服务号,服务号是当前占有锁的进程的排队号。
(2)每个进程申请锁的时候,首先申请一个排队号,然后轮询锁的服务号是否等于自己的排队号,如果等于,表示自己占有锁,可以进入临界区,否则继续轮询。
(3)当进程释放锁的时候,把服务号加 1,下一个进程看到服务号等于自己的排队号,退出自旋,进入临界区。

2.2.2 自旋锁的缺陷

自旋锁是一种比较低级的保护数据结构或代码片段的原始方式,这种锁可能存在两个问题:

(1)死锁。试图递归地获得自旋锁必然会引起死锁:例如递归程序的持有实例在第二个实例循环,以试图获得相同自旋锁时,就不会释放此自旋锁。所以,在递归程序中使用自旋锁应遵守下列策略:递归程序决不能在持有自旋锁时调用它自己,也决不能在递归调用时试图获得相同的自旋锁。此外如果一个进程已经将资源锁定,那么,即使其它申请这个资源的进程不停地疯狂“自旋”,也无法获得资源,从而进入死循环。

(2)过多占用cpu资源。如果不加限制,由于申请者一直在循环等待,因此自旋锁在锁定的时候,如果不成功,不会睡眠,会持续的尝试,单cpu的时候自旋锁会让其它process动不了。因此,一般自旋锁实现会有一个参数限定最多持续尝试次数。超出后,自旋锁放弃当前time slice,等下一次机会。

由此可见,自旋锁比较适用于锁使用者保持锁时间比较短的情况。正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用,而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。

2.2.3 使用方法

自旋锁的实现基于共享变量。一个线程通过给共享变量设置一个值来获取锁,其他等待线程查询共享变量是否为0来确定锁现是否可用,然后在忙等待的循环中“自旋”直到锁可用为止。自旋锁的状态值为1表示解锁状态,说明有1个资源可用;0或负值表示加锁状态,0说明可用资源数为0。

ARM 体系下的自旋锁相关的结构如下:
路径:kernel-4.9/include/linux/spinlock_types.h

typedef struct {
	volatile unsigned int lock;
} arch_spinlock_t;

typedef struct spinlock {
	union {
		struct raw_spinlock rlock;
		......
	};
} spinlock_t;

typedef struct raw_spinlock {
	arch_spinlock_t raw_lock;
	......
} raw_spinlock_t;

可以看到数据类型spinlock 对数据类型raw_spinlock 做了封装,spinlock 和 raw_spinlock(原始自旋锁)有什么关系?
Linux内核有一个实时内核分支(开启配置宏CONFIG_PREEMPT_RT)来支持硬实时特性,内核主线只支持软实时。

对于没有打上实时内核补丁的内核,spinlock只是封装raw_spinlock,它们完全一样。如果打上实时内核补丁,那么spinlock使用实时互斥锁保护临界区,在临界区内可以被抢占和睡眠,但raw_spinlock还是自旋锁。

目前主线版本还没有合并实时内核补丁,说不定哪天就会合并进来,为了使代码可以兼容实时内核,最好坚持3个原则:
(1)尽可能使用spinlock;
(2)绝对不允许被抢占和睡眠的地方,使用raw_spinlock,否则使用spinlock;
(3)如果临界区足够小,使用raw_spinlock。
各种处理器架构需要自定义数据类型arch_spinlock_t,ARM64架构的定义:
路径:arch/arm64/include/asm/spinlock_types.h

#include <asm-generic/qspinlock_types.h>
include/asm-generic/qspinlock_types.h
typedef struct qspinlock {
	union {
		atomic_t val;
 
		/*
		 * By using the whole 2nd least significant byte for the
		 * pending bit, we can allow better optimization of the lock
		 * acquisition for the pending bit holder.
		 */
#ifdef __LITTLE_ENDIAN		/* 小端格式 */
		struct {
			u8	locked;
			u8	pending;
		};
		struct {
			u16	locked_pending;
			u16	tail;
		};
#else						/* 大端格式 */
		struct {
			u16	tail;
			u16	locked_pending;
		};
		struct {
			u8	reserved[2];
			u8	pending;
			u8	locked;
		};
#endif
	};
} arch_spinlock_t;

定义并且初始化静态自旋锁的方法:

DEFINE_SPINLOCK(x)
等同于:spinlock_t x = SPIN_LOCK_UNLOCKED spin_is_locked(x)

在运行时动态初始化自旋锁的方法:

spin_lock_init(x)

申请自旋锁的函数:

序号函数说明
(1)void spin_lock(spinlock_t *lock);申请自旋锁,如果锁被其他处理器占有,当前处理器自旋等待
(2)void spin_lock_bh(spinlock_t *lock);申请自旋锁,并且禁止当前处理器的软中断
(3)void spin_lock_irq(spinlock_t *lock);申请自旋锁,并且禁止当前处理器的硬中断
(4)spin_lock_irqsave(lock, flags)申请自旋锁,保存当前处理器的硬中断状态,并且禁止当前处理器的硬中断。在解锁时需用spin_unlock _irqrestore 函数恢复中断的状态
(5)int spin_trylock(spinlock_t *lock);申请自旋锁,如果申请成功,返回1;如果锁被其他处理器占有,当前处理器不等待,立即返回0

当自旋锁需要用于中断上下文时, 必须使用spin_lock_irq 函数。

释放自旋锁的函数:

序号函数说明
(1)void spin_unlock(spinlock_t *lock);释放自旋锁
(2)void spin_unlock_bh(spinlock_t *lock);释放自旋锁,并且开启当前处理器的软中断
(3)void spin_unlock_irq(spinlock_t *lock);释放自旋锁,并且开启当前处理器的硬中断
(4)void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags);释放自旋锁,并且恢复当前处理器的硬中断状态

定义并且初始化静态原始自旋锁的方法:

DEFINE_RAW_SPINLOCK(x)

在运行时动态初始化原始自旋锁的方法:

raw_spin_lock_init(x)

申请原始自旋锁的函数:

序号函数说明
(1)raw_spin_lock(lock)申请原始自旋锁,如果锁被其他处理器占有,当前处理器自旋等待
(2)raw_spin_lock_bh(lock)申请原始自旋锁,并且禁止当前处理器的软中断
(3)raw_spin_lock_irq(lock)申请原始自旋锁,并且禁止当前处理器的硬中断
(4)raw_spin_lock_irqsave(lock, flags)申请原始自旋锁,保存当前处理器的硬中断状态,并且禁止当前处理器的硬中断
(5)raw_spin_trylock(lock)申请原始自旋锁,如果申请成功,返回1;如果锁被其他处理器占有,当前处理器不等待,立即返回0

释放原始自旋锁的函数:

序号函数说明
(1)raw_spin_unlock(lock)释放原始自旋锁
(2)raw_spin_unlock_bh(lock)释放原始自旋锁,并且开启当前处理器的软中断
(3)raw_spin_unlock_irq(lock)释放原始自旋锁,并且开启当前处理器的硬中断
(4)raw_spin_unlock_irqrestore(lock, flags)释放原始自旋锁,并且恢复当前处理器的硬中断状态

2.2.4 实例

假设simple_count 变量初始值为0, 并期望该变量的值不大于1 。在多CPU 的情况和可抢占内核中, 如下代码并不能保障simple_count 不出现大于l 的值:

int simple_count=O;

if(simple_count)
	return -EBUSY;
simple_count++;

例一:
下面这个例子演示使用自旋锁保护simple_count 变量的加操作, 确保该设备不被多个用户同时打开。
设备打开与关闭函数代码如下:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <linux/spinlock.h>

MODULE_LICENSE("GPL");

#define MAJOR_NUM 224

static ssize_t simplespin_read(struct file *, char *, size_t, loff_t*);
static ssize_t simplespin_write(struct file *, const char *, size_t, loff_t*);
static int simplespin_open(struct inode *inode, struct file *filp);
static int simplespin_release(struct inode *inode, struct file *filp);

struct file_operations simplespin_fops =
{
	.read=simplespin_read, 
	.write=simplespin_write, 
	.open=simplespin_open,
	.release=simplespin_release,
};

static int simplespin_count = 0;
static DEFINE_SPINLOCK(spin);
//static spinlock_t spin = SPIN_LOCK_UNCLOKED;

static int simplespin_open(struct inode *inode, struct file *filp)
{
	/*获得自选锁*/
	spin_lock(&spin);
	
	/*临界资源访问*/
	if (simplespin_count)
	{
		spin_unlock(&spin);
		return - EBUSY;
	}
	simplespin_count++;
	
	/*释放自选锁*/
	spin_unlock(&spin);
	return 0;
}

static int simplespin_release(struct inode *inode, struct file *filp)
{
	simplespin_count--;
	return 0;
}

static ssize_t simplespin_read(struct file *filp, char *buf, size_t len, loff_t*off)
{
	return 0;
}

static ssize_t simplespin_write(struct file *filp, const char *buf, size_t len,loff_t *off)
{
	return 0;
}

static int __init simplespin_init(void)
{
	int ret;
	spin_lock_init(&spin);
	/*注册设备驱动*/
	ret = register_chrdev(MAJOR_NUM, "chardev", &simplespin_fops);
	if (ret)
	{
		printk("chardev register failure\n");
	}
	else
	{
		printk("chardev register success\n");
	}
	return ret;
}

static void __exit simplespin_exit(void)
{
	/*注销设备驱动*/
	unregister_chrdev(MAJOR_NUM, "chardev");
}

module_init(simplespin_init);
module_exit(simplespin_exit);

应用层参考代码如下:

void main()
{
	int fd;
	fd=open("/dev/fgj",O_RDWR);
	if(fd==-1)
	{
		perror("error open\n");
		exit(-1);
	}
	printf("open /dev/fgj successfully\n");
	
	while(1);
	close(fd);
}

本例运行结果如下:

[root@urbetter drivers]# insmod spinlock.ko
chardev register success
[root@urbetter drivers]# mknod /dev/fgj c 224 0
[root@urbetter drivers]# .test&
[root@urbetter drivers]# open /dev/fgj successfully
再次打开会失败
[root@urbetter drivers]# ./test
error open
: Device or resource busy
[root@urbetter drivers]#

例二:
路径:kernel-4.9/drivers/leds/leds-ss4200.c

/*
 * This protects access to the gpio ports.
 */
static DEFINE_SPINLOCK(nasgpio_gpio_lock);

static void nasgpio_led_set_attr(struct led_classdev *led_cdev,
				 u32 port, u32 value)
{
	spin_lock(&nasgpio_gpio_lock);
	__nasgpio_led_set_attr(led_cdev, port, value);
	spin_unlock(&nasgpio_gpio_lock);
}

static u32 nasgpio_led_get_attr(struct led_classdev *led_cdev, u32 port)
{
	struct nasgpio_led *led = led_classdev_to_nasgpio_led(led_cdev);
	u32 gpio_in;

	spin_lock(&nasgpio_gpio_lock);
	gpio_in = inl(nas_gpio_io_base + port);
	spin_unlock(&nasgpio_gpio_lock);
	if (gpio_in & (1<<led->gpio_bit))
		return 1;
	return 0;
}

static int ich7_gpio_init(struct device *dev)
{
	spin_lock(&nasgpio_gpio_lock);
   ..........
	spin_unlock(&nasgpio_gpio_lock);
	return 0;
}

用DEFINE_SPINLOCK静态初始化自旋锁,在init和属性attr的操作中,涉及硬件读写的地方使用自旋锁。
例三:
路径:kernel-4.9/drivers/leds/leds-bcm6328.c

static void bcm6328_led_set(struct led_classdev *led_cdev,
			    enum led_brightness value)
{
	struct bcm6328_led *led =
		container_of(led_cdev, struct bcm6328_led, cdev);
	unsigned long flags;

	spin_lock_irqsave(led->lock, flags);
	*(led->blink_leds) &= ~BIT(led->pin);
	if ((led->active_low && value == LED_OFF) ||
	    (!led->active_low && value != LED_OFF))
		bcm6328_led_mode(led, BCM6328_LED_MODE_ON);
	else
		bcm6328_led_mode(led, BCM6328_LED_MODE_OFF);
	spin_unlock_irqrestore(led->lock, flags);
}

static int bcm6328_leds_probe(struct platform_device *pdev)
{
	spinlock_t *lock; 

	lock = devm_kzalloc(dev, sizeof(*lock), GFP_KERNEL);
	if (!lock)
		return -ENOMEM;
	spin_lock_init(lock);
	return 0;
}

用spin_lock_init动态初始化自旋锁,涉及硬件读写的地方使用spin_lock_irqsave和spin_unlock_irqrestore。

2.3 读写锁(也称读写自旋锁)

2.3.1 概述

读写自旋锁( rwlock,通常简称读写锁) 实际是一种特殊的自旋锁,是对自旋锁的改进。在实际应用中如果需要对某个数据的读操作和写操作进行有区别的加锁时, 可以使用读写锁。读写锁把对共享资源的访问者划分成读者和写者, 读者只对共享资源进行读访问, 写者则需要对共享资源进行写操作。

读写锁相对于普通自旋锁而言, 能提高并发性, 因为在多处理器系统中, 它允许同时有多个读者来访问共享资源。写者是排他性的, 一个读写锁同时只能有一个写者或多个读者,但不能同时既有读者又有写者。在读写锁保持期间也是抢占失效的。如果读写锁当前没有读者也没有写者, 那么写者可以立刻获得读写锁, 否则它必须自旋, 直到没有任何写者或读者。如果读写锁没有写者, 那么读者可以立即获得该读写锁, 否则读者必须自旋, 直到写者释放该读写锁。

在ARM平台上, 内核用一个无符号的32位整数来记录读写锁中读写线程的数量, 写锁使用最高位(只有一个),读锁使用其余31位(0~ 30bit)
(1)申请写锁时,如果计数值是0,那么设置计数的最高位,进入临界区;如果计数值不是0说明写者占有写锁或者读者占有读锁,那么自旋等待;
(2)申请读锁时,如果计数值的最高位为0,那么把计数加1,进入临界区;如果计数的最高位不是0,说明写者占有写锁,那么自旋等待;
(3)释放写锁时,把计数值设置为0;
(4)释放读锁时,把计数值减1。

2.3.2 使用方法

读写自旋锁的定义:

路径:include/linux/rwlock_types.h

typedef struct {
	arch_rwlock_t raw_lock;
    ...
} rwlock_t;

各种处理器架构需要自定义数据类型arch_rwlock_t,ARM64架构的定义:

typedef struct qrwlock {
	union {
		atomic_t cnts;
		struct {
#ifdef __LITTLE_ENDIAN
			u8 wlocked;	/* Locked for write? */
			u8 __lstate[3];
#else
			u8 __lstate[3];
			u8 wlocked;	/* Locked for write? */
#endif
		};
	};
	arch_spinlock_t		wait_lock;
} arch_rwlock_t;

定义并且初始化静态读写自旋锁的方法:

DEFINE_RWLOCK(x)

在运行时动态初始化读写自旋锁的方法:

rwlock_init(lock)

申请读锁的函数:

序号函数说明
(1)read_lock(lock)申请读锁,如果写者占有写锁,当前处理器等待
(2)read_lock_bh(lock) read_lock_bh(lock)申请读锁,并且禁止当前处理器的软中断
(3)read_lock_irq(lock)申请读锁,并且禁止当前处理器的硬中断
(4)read_lock_irqsave(lock, flags)申请读锁,保存当前处理器的硬中断状态,并且禁止当前处理器的硬中断
(5)read_trylock(lock)尝试申请读锁,如果没有占有写锁的写者,那么申请成功,返回1;如果写者占有写锁,那么当前处理器不等待,返回0

释放读锁的函数:

序号函数说明
(1)read_unlock(lock)释放读锁
(2)read_unlock_bh(lock)释放读锁,并且开启当前处理器的软中断
(3)read_unlock_irq(lock)释放读锁,并且开启当前处理器的硬中断
(4)read_unlock_irqrestore(lock, flags)释放读锁,并且恢复当前处理器的硬中断状态

申请写锁的函数:

序号函数说明
(1)write_lock(lock)申请写锁,如果写者占有写锁或者读者占有读锁,当前处理器自旋等待
(2)write_lock_bh(lock)申请写锁,并且禁止当前处理器的软中断
(3)write_lock_irq(lock)申请写锁,并且禁止当前处理器的硬中断
(4)write_lock_irqsave(lock, flags)申请写锁,保存当前处理器的硬中断状态,并且禁止当前处理器的硬中断
(5)write_trylock(lock)尝试申请写锁,如果没有占有锁的写者和读者,那么申请写锁成功,返回1;如果写者占有写锁或者读者占有读锁,那么当前处理器不等待,立即返回0

释放写锁的函数:

序号函数说明
(1)write_unlock(lock)释放写锁
(2)write_unlock_bh(lock)释放写锁,并且开启当前处理器的软中断
(3)write_unlock_irq(lock)释放写锁,并且开启当前处理器的硬中断
(4)write_unlock_irqrestore(lock, flags)释放写锁,并且恢复当前处理器的硬中断状态

2.3.3 实例

rwlock_t lock;
static ssize_t simplespin_read(struct file *, char *, size_t, loff_t*)
{
	int count=len;
	read_lock(&lock); //加读锁
	if(copy_to_user(buf,demoBuffer,count))
	{
		count = -EFAULT;
	}
	read_unlock(&lock); //释放读锁
	return count;
}

static ssize_t simplespin_write(struct file *, const char *, size_t, loff_t*)
{
	int count=len;
	write_lock(&lock); //加写锁
	if(copy_from_user(demoBuffer,buf,count))
	{
		count = -EFAULT;
	}
	write_unlock(&lock); //释放写锁
	return count;
}

static int __init simplespin_init(void)
{
	rwlock_init(&lock);
}

2.3.4 缺点

如果读者很多,写者很难获取写锁,可能饿死。假设有一个读者占有读锁,然后写者申请写锁,写者需要自旋等待,接着另一个读者申请读锁,它可以获取读锁,如果两个读者轮流占有读锁,可能造成写者饿死。

2.4 顺序锁(即顺序读写锁)

2.4.1 概述

针对读写自旋锁的缺点,内核实现了顺序读写锁,主要改进是:如果写者正在等待写锁,那么读者申请读锁时自旋等待,写者在锁被释放以后先得到写锁。顺序读写锁的配置宏是CONFIG_QUEUED_RWLOCKS,源文件是“kernel/locking/qrwlock.c”。

前面的读写自旋锁,更加偏向于读者。内核提供了更加偏向于写者的锁 —— seqlock(顺序锁)。

顺序锁区分读者和写者,和读写自旋锁相比,它的优点是不会出现写者饿死的情况。读者
不会阻塞写者,读者读数据的时候写者可以写数据。顺序锁有序列号,写者把序列号加1,如
果读者检测到序列号有变化,发现写者修改了数据,将会重试,读者的代价比较高。
顺序锁支持两种类型的读者。
(1)顺序读者(sequence readers):不会阻塞写者,但是如果读者检测到序列号有变化,
发现写者修改了数据,读者将会重试。
(2)持锁读者(locking readers):如果写者或另一个持锁读者正在访问临界区,持锁读
者将会等待。持锁读者也会阻塞写者。这种情况下顺序锁退化为自旋锁。
如果使用顺序读者,那么互斥访问的资源不能是指针,因为写者可能使指针失效,读
者访问失效的指针会出现致命的错误。
顺序锁比读写自旋锁更加高效,但读写自旋锁适用于所有场合,而顺序锁不能适用于所有场合,所以顺序锁不能完全替代读写自旋锁。

顺序锁有两个版本。
(1)完整版的顺序锁提供自旋锁和序列号。
(2)顺序锁只提供序列号,使用者有自己的自旋锁。

2.4.2 使用方法

2.4.2.1 完整版的顺序锁

完整版的顺序锁的定义如下:
路径:include/linux/seqlock.h

typedef struct {
	struct seqcount seqcount;
	spinlock_t lock;
} seqlock_t;

成员seqcount 是序列号,成员lock 是自旋锁。
定义并且初始化静态顺序锁的方法如下:

DEFINE_SEQLOCK(x)

运行时动态初始化顺序锁的方法如下:

seqlock_init(x)

顺序读者读数据的方法如下:

seqlock_t seqlock;
unsigned int seq;
do {
	seq = read_seqbegin(&seqlock);
	读数据
} while (read_seqretry(&seqlock, seq));

首先调用函数read_seqbegin 读取序列号,然后读数据,最后调用函数read_seqretry 判断序列号是否有变化。如果序列号有变化,说明写者修改了数据,那么读者需要重试。
持锁读者读数据的方法如下:

seqlock_t seqlock;
read_seqlock_excl(&seqlock);
读数据
read_sequnlock_excl(&seqlock);

函数read_seqlock_excl 的一些变体:

序号函数说明
(1)read_seqlock_excl_bh()申请自旋锁,并且禁止当前处理器的软中断。
(2)read_seqlock_excl_irq()申请自旋锁,并且禁止当前处理器的硬中断。
(3)read_seqlock_excl_irqsave()申请自旋锁,保存当前处理器的硬中断状态,并且禁止当前处理器的硬中断。

读者还可以根据情况灵活选择:如果没有写者在写数据,那么读者成为顺序读者;如果写者正在写数据,那么读者成为持锁读者。方法如下:

seqlock_t seqlock;
unsigned int seq = 0;
do {
	read_seqbegin_or_lock(&seqlock, &seq);
	读数据
} while (need_seqretry(&seqlock, seq));
done_seqretry(&seqlock, seq);

函数read_seqbegin_or_lock 的一个变体:

序号函数说明
(1)read_seqbegin_or_lock_irqsave如果没有写者在写数据,那么读者成为顺序读者;如果写者正在写数据,那么读者成为持锁读者,申请自旋锁,保存当前处理器的硬中断状态,并且禁止当前处理器的硬中断。

写者写数据的方法如下:

write_seqlock(&seqlock);
写数据
write_sequnlock(&seqlock);

函数write_seqlock 的一些变体:

序号函数说明
(1)write_seqlock_bh()申请写锁,并且禁止当前处理器的软中断。
(2)write_seqlock_irq()申请写锁,并且禁止当前处理器的硬中断。
(3)write_seqlock_irqsave()申请写锁,保存当前处理器的硬中断状态,并且禁止当前处理器的硬中断。

写者申请顺序锁的执行过程是:首先申请自旋锁,然后把序列号加1,序列号变成奇数。
写者释放顺序锁的执行过程是:首先把序列号加1,序列号变成偶数,然后释放自旋锁。

2.4.2.2 只提供序列号的顺序锁

只提供序列号的顺序锁的定义如下:
路径:include/linux/seqlock.h

typedef struct seqcount {
	unsigned sequence;} seqcount_t;

定义并且初始化静态顺序锁的方法如下:

seqcount_t x = SEQCNT_ZERO(x);

运行时动态初始化顺序锁的方法如下:

seqcount_init(s)

读者读数据的方法如下:

seqcount_t sc;
unsigned int seq;
do {
	seq = read_seqcount_begin(&sc);
	读数据
} while (read_seqcount_retry(&sc, seq));

写者写数据的方法如下:

spin_lock(&mylock);/* 假设使用者定义了自旋锁mylock */
write_seqcount_begin(&sc);
写数据
write_seqcount_end(&sc);
spin unlock(&mylock);

2.4.3 实例

路径:kernel-4.9/drivers/gpu/drm/drm_irq.c

static void store_vblank(struct drm_device *dev, unsigned int pipe,
			 u32 vblank_count_inc,
			 struct timeval *t_vblank, u32 last)
{
	struct drm_vblank_crtc *vblank = &dev->vblank[pipe];

	assert_spin_locked(&dev->vblank_time_lock);

	vblank->last = last;

	write_seqlock(&vblank->seqlock);
	vblank->time = *t_vblank;
	vblank->count += vblank_count_inc;
	write_sequnlock(&vblank->seqlock);
}

static u32 drm_vblank_count_and_time(struct drm_device *dev, unsigned int pipe,
				     struct timeval *vblanktime)
{
	struct drm_vblank_crtc *vblank = &dev->vblank[pipe];
	u32 vblank_count;
	unsigned int seq;

	do {
		seq = read_seqbegin(&vblank->seqlock);
		vblank_count = vblank->count;
		*vblanktime = vblank->time;
	} while (read_seqretry(&vblank->seqlock, seq));

	return vblank_count;
}

int drm_vblank_init(struct drm_device *dev, unsigned int num_crtcs)
{
	seqlock_init(&vblank->seqlock);
}

三、进程的其他互斥技术

下面介绍的3种互斥技术偏向于更底层。

3.1 禁止内核抢占

内核抢占是指当进程在内核模式下运行的时候可以被其他进程抢占,编译内核时需要打开配置宏CONFIG_PREEMPT。
支持抢占的内核称为抢占式内核,不支持抢占的内核称为非抢占式内核。个人计算机的桌面操作系统要求响应速度快,适合使用抢占式内核;服务器要求业务的吞吐率高,适合使用非抢占式内核。Android使用抢占式内核。
如果变量只会被本处理器上的进程访问,比如每处理器变量,可以使用禁止内核抢占的方法来保护,代价很低。如果变量可能被其他处理器上的进程访问,应该使用锁保护。
每个进程的thread_info 结构体有一个抢占计数器:“int preempt_count”,其中第0~7 位是抢占计数,第8~15 位是软中断计数,第16~19 位是硬中断计数,第20 位是不可屏蔽中断计数。
禁止内核抢占的时候把当前进程的抢占计数加1,开启内核抢占的时候把当前进程的抢占计数减1。
禁止内核抢占的编程接口如下:

preempt_disable()

开启内核抢占的编程接口如下:

序号函数说明
(1)preempt_enable()把抢占计数减1,如果抢占计数器变成0,重新调度进程。
(2)preempt_enable_no_resched()把抢占计数减1,如果抢占计数器变成0,不调度进程。

如果抢占计数器变成0,表示已经enable,可以抢占内核了。
申请自旋锁的函数包含了禁止内核抢占,其代码如下:
spin_lock() -> raw_spin_lock() -> _raw_spin_lock() -> __raw_spin_lock()
路径:include/linux/spinlock_api_smp.h

static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
	preempt_disable();LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}

释放自旋锁的函数包含了开启内核抢占,其代码如下:
spin_unlock() -> raw_spin_unlock() -> _raw_spin_unlock() -> __raw_spin_unlock()
路径:include/linux/spinlock_api_smp.h

static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{do_raw_spin_unlock(lock);
	preempt_enable();
}

禁止内核抢占偏CPU核心操作,一般的驱动不会调用。

3.2 禁止软中断

如果进程和软中断可能访问同一个对象,那么进程和软中断需要互斥,进程需要禁止软中断。

情况操作
如果进程只需要和本处理器的软中断互斥那么进程只需要禁止本处理器的软中断
如果进程要和所有处理器的软中断互斥那么进程需要禁止本处理器的软中断,还要使用自旋锁和其他处理器的软中断互斥。

每个进程的thread_info 结构体有一个抢占计数器“int preempt_count”,其中第8~15位是软中断计数。
禁止软中断的时候把当前进程的软中断计数加2,开启软中断的时候把当前进程的软中断计数减2。
禁止软中断的接口如下:

local_bh_disable()

注意:这个接口只能禁止本处理器的软中断,不能禁止其他处理器的软中断。bh 表示“bottom half”,即下半部,软中断是中断处理程序的下半部。
开启软中断的接口是:

local_bh_enable()

把当前进程的软中断计数减2,如果软中断计数、硬中断计数和不可屏蔽中断计数都是0,并且有软中断需要处理,那么执行软中断。

3.3 禁止硬中断

如果进程和硬中断可能访问同一个对象,那么进程和硬中断需要互斥,进程需要禁止硬中断。

情况操作
如果进程只需要和本处理器的硬中断互斥那么进程只需要禁止本处理器的硬中断
如果进程要和所有处理器的硬中断互斥那么进程需要禁止本处理器的硬中断,还要使用自旋锁和其他处理器的硬中断互斥。

禁止硬中断的接口如下:

1local_irq_disable()。
(2local_irq_save(flags):首先把硬中断状态保存在参数flags 中,然后禁止硬中断。

这两个接口只能禁止本处理器的硬中断,不能禁止其他处理器的硬中断。禁止硬中断
以后,处理器不会响应中断请求。

开启硬中断的接口如下:

1local_irq_enable()。
(2local_irq_restore(flags):恢复本处理器的硬中断状态。

local_irq_disable()和local_irq_enable()不能嵌套使用,local_irq_save(flags)和local_irq_restore(flags)可以嵌套使用。

四、进一步提高并行性能

在多处理器系统中,为了提高程序的性能,需要尽量减少处理器之间的互斥,使处理器可以最大限度地并行执行。从互斥信号量到读写信号量的改进,从自旋锁到读写自旋锁的改进,允许读者并行访问临界区,提高了并行性能,但是我们还可以进一步提高并行性能,使用下面这些避免使用锁的互斥技术。

4.1 每处理器变量

4.2 每处理器计数器

4.3 内存屏障

4.2 RCU

4.2 可睡眠RCU

参考:
[1]:http://www.wowotech.net/kernel_synchronization/atomic.html
[2]:https://blog.csdn.net/u010299133/article/details/100179752
[3]:https://blog.csdn.net/weixin_34932795/article/details/116612353
[4]:Linux内核深度解析

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值