Linux驱动进阶(一)——设备驱动中的并发控制

文章目录

  • 前言
  • 并发与竞争
  • 原子变量操作
    • 原子变量操作
    • 原子整型操作
    • 原子位操作
  • 自旋锁
    • 自旋锁概述
    • 自旋锁的使用
    • 自旋锁的使用注意事项
  • 信号量
    • 信号量概述
    • 信号量的实现
    • 信号量的使用
    • 自旋锁与信号量的对比
  • 完成量
    • 完成量概述
    • 完成量的实现
    • 完成量的使用
  • 小结


前言

现代操作系统有三大特征:中断处理、多任务处理和多处理器(SMP)。这些特性导致当多个进程、线程或者CPU同时访问一个资源时,可能会发生错误,这些错误是操作系统运行所不允许的。在操作系统中,内核需要提供并发控制机制,对公共资源进行保护。本章将对保护这些公共资源的方法进行简要的介绍。

并发与竞争

并发是指在操作系统中,一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一处理机上运行,但任一时刻点上只有一个程序在处理机上运行。并发容易导致竞争问题。竞争就是两个或两个以上的进程同时访问一个资源,从而引起资源的错误。
例如,在数据库中,允许多个用户同时访问和更改共享数据的进程。允许多个用户同时访问和更改共享数据就很可能发生冲突。以飞机售票为例,会引起数据不一致的错误。
例如,飞机订票系统中的一个活动序列:
(1)甲售票员读出某航班的机票余额为A,设A=16;
(2)乙售票员读出同一航班的机票余额A,也为16;
(3)甲售票员点卖出一张机票,修改机票的余额为A=A-1,即A=15,把A写回数据库;
(4)乙售票员也卖出一张机票,修改机票余额A=A-1,即是A=15,把A写回数据库。
结果:卖出两站机票,但数据库中机票余额只减少1张。这种情况就是并发导致的问题。本章将介绍一些机制避免并发对系统资源的影响。这些并发控制机制有原子变量操作、自旋锁、信号量和完成量。下面对这几种机制进行详细的介绍。

原子变量操作

原子变量操作是Linux中提供的一种简单的同步机制,是一种在操作过程中不会被打断的操作,所以在内核驱动程序中非常有用。本节对Linux中原子变量的操作进行详细的分析。

原子变量操作

所谓原子变量操作,就是该操作绝不会在执行完毕前被任何其他任务或事件打断。也就是说,原子变量操作是一种不可打断的操作。原子操作需要硬件的支持,因此是架构相关的,其API和原子类型的定义都在内核源码树的include/asm/atomic.h文件中,它们都使用汇编语言实现,因为C语言并不能实现这样的操作
原子变量操作不会只执行一半,又去执行其他代码。它要么执行完毕,要么一点也不执行。原子变量操作的优点是编写简单;缺点是功能太简单,只能做计数操作,保护东西太少,但却是其他同步手段的基石。在Linux中,原子变量的定义的如下:

typedef struct {
	volatile int counter;
}atomic_t;

关键字volatile用来暗示GCC不要对该类型数据优化,所以对这个变量counter的访问都是基于内存的,不要将其缓冲到寄存器中。存储到寄存器中,可能导致内存中的数据已经改变,而寄存器中的数据没有改变
在Linux中,定义了两种原理变量操作方法,一种是原子整型操作,另一种是原子位操作。下面分别对这两种原子变量操作方法进行分析。

原子整型操作

有时候需要共享的资源可能只是一个简单的整型数值。例如在驱动程序中,需要对包含一个count的计数器。这个计数器表示有多个应用程序打开了设备所对应的设备文件。通常在设备驱动程序的open()函数中,将count变量加1。在close()函数中,将count减1。如果只有一个应用程序执行打开和关闭操作,那么这里的count计数就不会出现问题。但是如果有多个应用程序同时打开或者关闭设备文件,那么就可能导致count多加或者少加,出现错误。
为了避免这个问题,内核提供了一个原子整型变量,称为atomic_t。该变量的定义如下:

typedef struct {
	volatile int counter;
}aotmic_t;

一个atomic_t变量实际上是一个int类型的值,但是由于一些处理器的限制,该int类型的变量不能表示完整的整数范围,只能表示24位数的范围。在SPARC处理器架构上,对原子操作缺乏指令级的支持,所以只能将32位中的低8位设置成一个锁,用户保护整型数据的并发访问。
在Linux中,定义一个atomic_t类型的变量与C语言中定义个类型的变量没有什么不同。例如,下面的的代码定义了前面说的count计数器。

atomic_t count;

这句话定义了一个atomic_t类型的count变量,atomic_t类型的变量只能通过Linux内核中定义的专用函数来操作,不能在变量上直接加1或者减1。下面介绍一下Linux中针对atomic_t类型的变量的操作函数。
1.定义atomic_t变量
ATOMIC_INIT宏的功能是定义一个atomic_t类型的变量,宏的参数是需要该变量初始化的值。该宏定义如下:

#defien ATOMIC_INIT(i) {(i)}

因为atomic_t类型的变量是一个结构体类型,所以对其进行定义和初始化应该用结构体定义和初始化的方法。例如定义一个名为count的atomic_t类型的变量的方法,代码如下:

atomic_t count = ATOMIC_INIT(0);

这句代码展开后,就是一个定义和初始化一个结构体的方法,展开后的代码如下:

atomic_t count = {(0)};

2.设置atomic_t变量的值
atomic_set(v,i)宏用来设置v变量的值为i。其定义如下代码所示。

#define atomic_set(v, i)   (((v)->counter) = i)

3.读atomic_t变量的值
atomic_read(v)宏来读v变量的值。其定义如下代码所示。

#define atomic_read(v)   ((v)->counter)

该宏对原子类型的变量进行原子读操作,它返回原子类型的变量v的值。
4.原子变量的加减法
atomic_add()函数用来将第一个参数i的值加到第二个参数v中,并返回一个void值。返回空的原因是将耗费更多的CPU时间,而大多数情况下原子变量的加法不需要返回值。atomic_add()函数的原型如下代码所示。

static inline void atomic_add(int i, volatile atomic_t *v)

atomic_add()函数功能相反的函数是atomic_sub()函数,该函数从原子变量v中减去i的值。atomic_sub()函数的原型如下代码所示。

static inline void atomic_sub(int i, volatile atomic_t *v)

5.原子变量的自加自减
atomic_inc()函数用来将v指向的变量加1,并返回一个void值。返回空的原因是将耗费更多的CPU时间,而大多数情况下原子变量的加法不需要返回值。atomic_inc()函数的原型如下代码所示。

static inline void atomic_inc(volatile atomic_t *v)

atomic_inc()函数功能相反的函数是atomic_dec()函数,该函数从原子变量v中减去1。atomic_dec()函数的原型如下代码所示:

static inline void atomic_dec(volatile atomic_t *v)

6.加减测试
atomic_inc_and_test()函数用来将v指向的变量加1,如果结果是0,则字节返回真;如果是非0,则返回假。atomic_inc_and_test()函数原型如下代码所示。

static inline int atomic_inc_and_test(volatile atomic_t *v)

atomic_inc_and_test()函数功能相反的函数是atomic_dec_and_test()函数,该函数从原子变量v中减去1。如果结果是0,则字节返回真;如果返回是非0,则返回假。 atomic_dec_and_test()函数的原型如下代码所示。

static inline int atomic_dec_and_test(volatile atomic_t *v)

综上所述,atomic_t类型的变量必须使用上面介绍的函数来访问,如果试图将原子变量看作整型变量来使用,则会出现编译错误。

原子位操作

除了原子整型操作外,还有原子位操作。原子位操作是根据数据的每一位单独进行操作。根据体系结构不同,原子位操作函数的实现也不同。这些函数的原型如下代码所示。

static inline void set_bit(int nr, volatile unsigned long *addr)
static inline void clear_bit(int nr, volatile unsigned long *addr)
static inline void change_bit(int nr, volatile unsigned long *addr)
static inline int test_and_set_bit(int nr, volatile unsigned long *addr)
static inline int test_and_clear_bit(int nr, volatile unsigned long *addr)
static inline int test_and_change_bit(int nr, volatile unsigned long *addr)

需要注意的是,原子位操作和原子整型操作是不同的。原子位操作不需要专门定义一个类似atomic_t类型的变量,只需要一个普通的变量指针就可以了。下面对上面的几个函数进行简要的分析:

  • set_bit()函数将addr变量的第nr位设置为1.
  • clear_bit()函数将addr变量的第nr位设置为0.
  • change_bit()函数将addr变量的第nr位设置为相反的数.
  • test_and_set_bit()函数将addr变量的第nr位设置为1,并返回没有修改之前的值.
  • test_and_clear_bit()函数将addr变量的第nr位设置为0,并返回没有修改之前的值.
  • test_and_change_bit()函数将addr变量的第nr位设置为相反的数,并返回没有修改之前的值.
    在Linux中,还定义了一组与原子位操作功能相同但非原子位的操作。这些函数的命名是在原子位操作的函数前加两个下划线。例如,与原子位操作set_bit()函数相对应的是__set_bit()函数,这个函数不会保证是一个原子操作。与此类似的函数原型如下所示。
static inline void __set_bit(int nr, volatile unsigned long *addr)
static inline void __clear_bit(int nr, volatile unsigned long *addr)
static inline void __change_bit(int nr, volatile unsigned long *addr)
static inline void __test_and_set_bit(int nr, volatile unsigned long *addr)
static inline void __test_and_clear_bit(int nr, volatile unsigned long *addr)
static inline void __test_and_change_bit(int nr, volatile unsigned long *addr)

自旋锁

自旋锁是一种简单的并发控制机制,其是实现信号量和完成量的基础。自旋锁对资源有很好的保护作用,在Linux驱动程序中进程使用,本节将对自旋锁进行详细的介绍。

自旋锁概述

在Linux中提供了一些锁机制来避免竞争条件,最简单的一种就是自旋锁。引入锁的机制,是因为单独的原子操作不能满足复杂的内核设计需要。例如,当一个临界区域要在多个函数之间来回运行时,原子操作就显得无能为力了。
Linux中一般可以认为有两种锁,一种是自旋锁,另一种是信号量。这两种锁是为了解决内核中遇到的不同问题开发的。其实现机制和应用场合有所不同,下文将分别对两种锁机制进行介绍。

自旋锁的使用

在Linux中,自旋锁的类型为struct spinlock_t。内核提供了一系列的函数对struct spinlock_t进行操作。下面对自旋锁的操作方法进行简要的介绍。
1.定义和初始化自旋锁
在Linux中,定义自旋锁的方法和定义普通结构体的方法相同,定义方法如下代码所示。

spinlock_t lock;

一个自旋锁必须初始化才能被使用,对自旋锁的初始化可以在编译阶段通过宏来实现,初始化自旋锁可以使用宏SPIN_LOCK_UNLOCKED,这个宏表示一个没有锁定的自旋锁,其代码形式如下代码所示。

spinlock_t lock=SPIN_LOCK_UNLOCKED; /*初始化一个未使用的自旋锁*/

在运行阶段,可以使用spin_lock_init()函数动态地初始化一个自旋锁,这个函数的原型如下:

void spin_lock_init(spinlock_t lock)

2.锁定自旋锁
在进入临界区前,需要使用spin_lock宏来获得自旋锁。spin_lock宏的代码如下:

#define spin_lock(lock)   _spin_lock(lock)

这个宏用来获得lock自旋锁,如果能够立即获得自旋锁,则宏立刻返回;否则,这个锁会一直自旋在哪里,直到该锁被其他线程释放为止。
3.释放自旋锁
当不再使用临界区时,需要使用spin_unlock宏释放自旋锁。spin_unlock宏的代码如下:

#define spin_unlock(lock)  _spin_unlock(lock)

这个宏用来释放lock自旋锁,当调用该宏之后,锁立刻被释放。
4.使用自旋锁
这里给出一个自旋锁的使用方法,首先是定义自旋锁,然后初始化、获得自旋锁和释放自旋锁。其代码如下:

spinlock_t lock;
spin_lock_init(&lock);
spin_lock(&lock);
//临界资源
spin_unlock(&lock);

在驱动程序中,有些设备只允许打开一次,那么就需要一个自旋锁保护表示设备的打开或者关闭状态的变量count。此处count属于一个临界资源,如果不对count进行保护,当设备打开频繁时,可能出现错误的count计数,所以必须对count进行保护。使用自旋锁包含count的代码如下:

int count=0;
spinlock_t lock;
int xxx_init(void)
{
	...
	spin_lock_init(&lock);
	....
}
/*文件打开函数*/
int xxx_open(struct inode *inode, struct file *filp)
{
	...
	spin_lock(&lock);
	if(count)
	{
		spin_unlock(&lock);
		return -EBUSY;
	}
	count++;
	spin_unlock(&lock);
	...
}
/*文件释放函数*/
int xxx_release(struct inode *inode, struct file *filp)
{
	...
	spin_lock(&lock);
	count--;
	spin_unlock(&lock);
	...
}

自旋锁的使用注意事项

在使用自旋锁时,有几个注意事项需要理解,这几个注意事项是:

  • 自旋锁是一种忙等待。Linux中,自旋锁当条件不满足时,会一直不断地循环条件是否被满足。如果满足,就解锁,继续运行下面的代码。这种忙等待机制是否对系统的性能有所影响呢?答案是肯定的。内核这样设计自旋锁确定对系统的性能有所影响,所以在实际编程中,程序员应该注意自旋锁不应该长时间地持有,它是一种适合短时间锁定的轻量级的加锁机制。
  • 自旋锁不能递归使用。这是因为,自旋锁被设计成在不同线程或者函数之间同步。如果一个线程在已经持有自旋锁时,其处于忙等待状态,则已经没有机会释放自己持有的锁。如果这时再调用自身,则自旋锁永远没有执行的机会了。所以类似下面的递归形式是不能使用自旋锁的。
void A()
{
	//锁定自旋锁
	A();
	//解锁自旋锁;
}

信号量

本节介绍锁的另一种实现机制,这种机制就是Linux中常用的信号量。Linux中提供两种信号量,一种用于内核程序中,一种用于应用程序中。由于这里讲解的是内核编程的知识,所以只对内核中的信号量进行详细讲述。

信号量概述

和自旋锁一样,信号量也是保护临界资源的一种有用方法。信号量与自旋锁的使用方法基本一样。与自旋锁相比,信号量只有当得到信号量的进程或者线程时才能够进入临界区,执行临界代码。信号量与自旋锁的最大不同点在于,当一个进程试图去获取一个已经锁定的信号量时,进程不会像自旋锁一样在远处忙等待,在信号量中采用另一种方式,这种方式如下所述。
当获取的信号量没有释放时,进程会将自身加入一个等待队列中去睡眠,直到拥有信号量的进程释放信号量后,处于等待队列中的那个进程才被唤醒。当进程唤醒后,就立刻重新从睡眠的地方开始执行,又一次试图获得信号量,当获得信号量后,程序继续执行。
从信号量的原理上来说,没有获得信号量的函数可能睡眠。这就要求只有能够睡眠的进程才能够使用信号量,不能睡眠的进程不能使用信号量。例如在中断处理程序中,由于中断需要立刻完成,所以不能睡眠,也就说在中断处理程序中是不能使用信号量的

信号量的实现

根据不同的平台,其提供的指令代码有所不同,所以信号量的实现也有所不同。在Linux中,信号量的定义如下代码所示。

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

下面详细介绍这个结构体的成员变量:
1.lock自旋锁
lock自旋锁的功能比较简单,用来对count变量起保护作用。当count要变化时,内部会锁定lock锁,当修改完成后,会释放lock锁。
2.count变量
count是信号量中一个非常重要的成员变量,这个变量可能取下面的3种值。

  • 等于0的值:如果这个值等于0,表示信号量被其他进程使用,现在不可以用这个信号量,但是wait_list队列种没有进程在等待信号量。
  • 小于0的值:如果这个值小于0,那么表示至少有一个进程在wait_list队列种等待信号量被释放。
  • 大于0的值:如果这个值大于0,表示这个信号量是空闲的,程序可以使用这个信号量。
    从这里可以看出信号量与自旋锁的一个不同是,自旋锁只能允许一个进程持有自旋锁,而信号量可以根据count的值,设定可以有多个进程持有这个信号量。根据count的取值,可以将信号量分为二值信号量和计数信号量。
    二值信号量就是count初始化时,被设置为1时的使用量,这种类型的信号量可以强制二者同一时刻只有一个运行。
    计数信号量,其允许一个时刻有一个或者多个进程同时持有信号量。具体有多少个进程可以持有信号量,取决于count的取值。
    3.等待队列
    wait是一个等待队列的链表头,这个链表将所有等待该信号量的进程组成一个链表结构。在这个链表中,存放了正在睡眠的进程链表。

信号量的使用

在Linux中,信号量的类型为struct semaphore。内核提供了一系列的函数对struct semaphore进行操作。下面将对信号量的操作方法进行简要的介绍。
1.定义和初始化信号量
在Linux中,定义信号量的方法和定义普通结构体的方法相同,定义方法如下代码所示。

struct semaphore sema;

一个信号量必须初始化才能被使用,sema_init()函数用来初始化信号量,并设置sema中count的值为val。其代码形式如下所示。

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

另一个宏可以初始化一个信号量的值为1的信号量,这种信号量又叫互斥体,其定义如下:

#define init_MUTEX(sem) sema_init(sem, 1)

该宏用于初始化一个互斥的信号量,并将这个信号量sem的值设置为1,等同于sema_init(sem ,1)。另一个宏init_MUTEX_LOCKED也用来初始化一个信号量,其将信号量sem的值设置为0,定义如下:

#define init_MUTEX_LOCKED(sem)  sema_init(sem, 0)

2.锁定信号量
在进入临界区前,需要使用down()函数来获取信号量。down()函数的代码如下:

void down(struct semaphore *sem)

该函数会导致睡眠,所以不能在中断上下文使用。另一个函数与down()函数相似,其代码如下:

int down_interruptible(struct semaphore *sem)

该函数与down()函数非常相似,不同之处在于,down()函数进入睡眠之后,就不能够被信号唤醒。而down_interruptible()函数进入睡眠后可以被信号唤醒。如果被信号唤醒,那么会返回非0值。所以在调用down_interruptible()函数时,一般应该检查返回值,判断被唤醒的原因。其代码如下:

if(down_interruptible(&sem))
{
	return -ERESTARTSYS;
}

3.释放信号量
当不再使用临界区时,需要使用up()函数释放信号量,up()函数代码如下:

void up(struct semaphore *sem)

4.使用信号量
下面给出一个信号量的使用方法,首先是定义信号量,然后初始化,获得信号量和释放信号。其代码如下:

struct semaphore sem;
int xxx_init(void)
{
	...
	init_MUTEX(&lock);
	....
}
/*文件打开函数*/
int xxx_open(struct inode *inode, struct file *filp)
{
	...
	down(&sem);
	/*不允许其他进程访问这个程序的临界资源*/
	...
	return 0;
}
/*文件释放函数*/
int xxx_release(struct inode *inode, struct file *filp)
{
	...
	up(&sem);
	...
}

5.信号量用于同步操作
前面已经说过,如果信号量被初始化为0,那么又可以将信号量叫做互斥体。互斥体可以用来实现同步的功能。同步表示一个线程的执行需要依赖于另一个线程的执行,这样可以保证线程的执行先后顺序。如下图所示,线程A执行到被保护代码A之前,一直处于睡眠状态。直到线程B执行完被保护代码B并调用up()函数后,才会执行被保护代码A。信号量的同步操作对于很多驱动程序来说是非常有用的,需要引起程序员的注意。
在这里插入图片描述

自旋锁与信号量的对比

自旋锁和信号量是解决并发控制的两个很重要的方法。在使用时,应该如何选择它们其中的一种方法呢?这要根据被包含资源的特定来确定。
自旋锁是一种最简单的保护机制,从上面的代码分析可以看出,自旋锁的定义只有一个结构体成员。当被包含的代码能够在很短的时间内执行完成时,那么使用自旋锁是一种很好的选择。因为自旋锁只是忙等待,不会进入睡眠。要知道,睡眠是一种非常浪费时间的操作。
信号量用来在多个进程之间互斥。信号量的执行可能引起进程的睡眠,睡眠需要进程上下文的切换,这是非常浪费时间的一项工作。所以只有在一个进程对被保护资源的占用时间比进程切换的时间长很多时,信号量才是一种更好的选择,否则,会降低系统的执行效率。

完成量

在驱动程序开发中,一种常见的情况是:一个线程需要等待另一个线程执行完某个操作后,才能继续执行。前面讲的信号量其实也能够完成这种工作,但其效率比Linux中专门针对这种情况的完成量机制要差一些。本节将对完成量进行详细的介绍。

完成量概述

Linux中提供了一种机制,实现一个线程发送一个信号通知另一个线程开始完成某个任务,这种机制就是完成量。完成量的目的告诉一个线程某个事件已经发生,可以在此事件基础上做你想做的另一个事件了。其实完成量和信号量比较类似,但是在这种线程通信的情况下,使用完成量有更高的效率。在内核中,可以通过进程看见使用完成量的代码。完成量是一种轻量级的机制,这种机制在一个线程希望告诉另一个线程某个工作已经完成的情况下是非常有用的。

完成量的实现

完成量是实现两个任务之间同步的简单方法,在内核种完成量由struct completion结构体表示。该结构体定义在include\linux\completion.h文件中,其定义如下代码如是。

struct completion{
	unsigned int done;
	wait_queue_head_t wait;
}

下面详细介绍这个结构体的两个成员变量。
1.done成员
done成员用来维护一个计数。当初始化一个完成量时,done成员被初始化为1.由done的类型可以知道这是一个无符号类型,其值永远大于0.当done等于0时,会将拥有完成量的线程置于等待状态;当done的值大于0时,表示等待完成量的函数可以立刻执行,而不需要等待。
2.wait成员
wait是一个等待队列的链表头,这个链表将所有等待该完成量的进程组成一个链表结构。在这个链表中,存放了正在睡眠的进程链表。

完成量的使用

在Linux中,信号量的类型为struct completion。内核提供一系列的函数对struct completion进行操作。下面对完成量的操作方法进行简要介绍。
1.定义和初始化完成量
在Linux中,定义完成量的方法和定义普通结构体的方法相同,定义方法如下:

struct completion com;

一个完成量必须初始化才能被使用,init_completion()函数用来初始化完成量。其定义如下:

static inline void init_completion(struct completion *x)
{
	x->done = 0;
	init_waitqueue_head(&x->wait);  /*初始化等待队列头*/
}

还可以使用宏DECLARE_COMPLETION定义和初始化一个变量,定义如下:

#define DECLEAR_COMPLETION(work) \
	struct completion work = COMPLETION_INITIALIZER(work)
#define COMPLETION_INITIALIZER(work) \
	{0__WAIT_QUEUE_HEAD_INITIALIZER((work).wait)}

仔细分析这个宏,可以发现其宏和init_completion()函数实现的功能一样,只是定义和初始化一个完成量的简单实现而已。
2.等待完成量
当要实现同步时,可以使用wait_for_completion()函数等待一个完成量。其函数代码如下:

void __sched wait_for_completion(struct completion *x)

该函数会执行一个不会被信号中断的等待。如果调用这个函数之后,没有一个线程完成这个完成量,那么执行wait_for_completion()函数的线程会一直等待下去,线程将不可以退出。
3.释放完成量
当需要同步的任务完成之后,可以使用下面的两个函数唤醒完成量。当唤醒之后wait_for_completion()函数之后的代码才可以继续执行。这两个函数的定义如下:

void complete(struct completion *x)
void completion_all(struct completion *x)

前者只唤醒一个等待的进程或者线程,后者将唤醒所有等待的进程或者线程。
4.使用完成量
下面给出一个完成量的使用方法,首先是定义完成量,然后初始化、获得完成量和释放完成量。其代码如下:

struct completion com;
int xxx_init(void)
{
	...
	init_completion(&com);
	...
}
int xxx_A()
{
	...
	/*代码A*/
	wait_for_completion(&com);
	/*代码B*/
	...
	return 0;
}
int XXX_B()
{
	...
	/*代码C*/
	complete(&com);
	...
}

代码中,xxx_init()函数完成了完成量的初始化。在xxx_A()函数中代码会一直执行到wait_for_completion()函数,如果此时complete->done值为0,那么线程会进入睡眠。如果此时的值大于0,那么wait_for_completion()函数会将complete->done的值减1,然后执行代码B的部分。
在执行xxx_B()函数的过程中,无论如何C代码都可以顺利执行,compete()函数会将complete->done的值加1,然后唤醒complete->wait中的一个线程。如果碰巧这个线程是执行的xxx_A()函数线程,那么会将这个线程从complete->wait()队列中唤醒并执行。

小结

本章介绍了Linux中内核的并发控制,分别介绍了完成并发控制功能的原子变量操作、自旋锁、信号量和完成量。这些都是内核中广泛使用的机制。每种机制都有自己的一些特点和使用范围。读者在使用时,应该对这些特定进行比较,选择出适合要求的并发控制机制。只有这样,才可以写出高效稳定的程序。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/36303.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

frp内网穿透

frp内网穿透 一.frp的作用和原理图 1.首先frp分客户端和服务端,frp客户端和服务端在同一个局域网。 2.frp服务端拥有公网ip与互联网连通。 frp的作用: 通过一台公司拥有外网ip的服务器做为frp服务端,通过请求转发的形式,转发到公…

Qt,day4

闹钟 #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent) :QWidget(parent),ui(new Ui::Widget) {ui->setupUi(this);this->setWindowTitle("闹钟");this->setWindowIcon(QIcon("D:\\HQYJRJ\\QT\\day1\\…

二十三种设计模式第十六篇--观察者模式

观察者模式是一种行为型设计模式,它建立了一种对象间的一对多依赖关系,使得当一个对象的状态发生变化时,所有依赖于它的对象都会得到通知并自动更新。这种模式可以实现对象间的松耦合通信,提高系统的可扩展性和灵活性。 观察者模…

TCP连接管理(三次握手,四次挥手)

目录 一、回顾一下TCP包头二、连接的建立——“三次握手”三、连接的建立——“四次挥手”保活计时器 一、回顾一下TCP包头 源端口号(Source Port):16 位字段,表示发送方的端口号。 目的端口号(Destination Port&…

FTP服务器

文章目录 FTP服务器FTP的数据传输原理FTP的功能简介不同等级的用户身份命令记录与日志文件记录限制用户活动的目录 FTP的工作流程与使用到的端口FTP主动式连接FTP被动式连接 vsftpd服务器基础设置为什么使用vsftpd所需要的软件以及软件结构vsftpd.conf 配置值说明与服务器环境比…

Leetcode---352周赛

周赛题目 2760. 最长奇偶子数组 2761. 和等于目标值的质数对 2762. 不间断子数组 2763. 所有子数组中不平衡数字之和 一、最长奇偶子数组 这题的数据范围允许用暴力来做,只要我们分别枚举左端点left和右端点right,然后看区间[left,right]是否符合题目条…

vue3 报错解决:找不到模块‘xxx.vue’或其相应的类型声明。(Vue 3 can not find module)

src下面建立一个xx.d.ts的文件 declare module *.vue {import { ComponentOptions } from vueconst componentOptions: ComponentOptionsexport default componentOptions }

数据结构错题集 第七章 查找

7.2 124 等比 1(1-2^h)/(1-2) 2^h - 1 查找失败的最小次数相等吗? 13.A D 推一下公式 (M1)/2 平均查找长度 17.有序 就可二分查找 记住向下取整就是往右 13题就是个例子 向上取整就是往左 7.3 A错 不会分裂 不是平衡树 12。 C 黑高…

Tomcat、Maven以及Servlet的基本使用

Tomcat什么是TomcatTomcat的目录结构启动Tomcat MavenMaven依赖管理流程配置镜像源 Servlet主要工作实现Servlet添加依赖实现打包分析 配置插件 Tomcat 什么是Tomcat Tomcat 是一个 HTTP 服务器。前面我们已经学习了 HTTP 协议, 知道了 HTTP 协议就是 HTTP 客户端和 HTTP 服务…

Android性能优化(bin启动优化)

我们平时会在android里面写个bin程序来干点活,但是有时候我们会发现很奇怪的现象,我明明很早就启动这个bin了,但是过了很久bin程序的main函数才被调用~。这个是为啥呢?主要有2个原因: 一.bin程序依赖的so库太多&#…

Python:使用prometheus-client提交数据到实现prometheus+ grafana数据监控

相关资料 prometheus文档:https://prometheus.io/grafana文档:https://grafana.com/grafana github: https://github.com/grafana/grafanaPyhton客户端https://pypi.org/project/prometheus-client/ 目录 1、使用Python提供数据源2、启动 prometheus3、…

zabbix 监控 windows 系统、java应用、SNMP

Zabbix 监控 Windows 系统 1、下载 Windows 客户端 Zabbix agent 2 2、安装客户端,配置 3.在服务端 Web 页面添加主机,关联模板 Zabbix 监控 java 应用 1、客户端开启 java jmxremote 远程监控功能 1.1配置 java jmxremote 远程监控功能 1.2启动…

左神算法之中级提升(2)

目录 [案例1】 【题目描述】 【思路解析1】 【思路解析2】 【代码实现】 【案例2】 【题目描述】 【思路解析】 【代码实现】 【案例3】 【题目描述】 【思路解析】 【代码实现】 【案例4】 【题目描述】今日头条2018面试题 第四题 【输入描述】 【思路解析】 【…

pytorch学习第一篇:conda配置jupyter notebooks pytorch

安装jupyter notebooks 创建一个pytorch的环境 conda create -n pytorch python3.10 conda activate pytorch安装jupyter notebook,运行命令 conda install jupyter notebook启动jupyter 运行命令 jupyter notebook或者 notebook查看pyhton版本 import sys p…

C++ 环境设置

不好意思,最近有点事,没更新。 C 环境设置 本地环境设置 如果您想要设置 C 语言环境,您需要确保电脑上有以下两款可用的软件,文本编辑器和 C 编译器。 文本编辑器 这将用于输入您的程序。文本编辑器包括 Windows Notepad、O…

QT - 20230710

练习&#xff1a;实现一个简易闹钟 widget.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QDateTime> #include <QDebug> #include <QTextToSpeech>namespace Ui { class Widget; }class Widget : public QWidget {Q_OBJECTpubl…

【数据挖掘】时间序列教程【十】

5.4 通用卡尔曼滤波 上一节中描述的状态空间模型作为观测方程的更一般的公式 和状态方程 这里是一个p1 向量

win系统电脑在线打开sketch文件的方法

自Sketch诞生以来&#xff0c;只有Mac版本。Windows计算机如何在线打开Sketch文件&#xff1f; 即时设计已经解决了你遇到的大部分问题&#xff0c;不占用内存也是免费的。 您可以使用此软件直接在线打开Sketch文件&#xff0c;完整预览并导出CSS、SVG、PNG等&#xff0c;还具…

pycharm的一些常用设置

pycharm的一些常用设置 1、最新安装pycharm ,怎么设置解释器如图&#xff1a; 2、可通过鼠标放大缩小配置&#xff1a; 进入setting>Editor>File and Code Templates&#xff0c;点击python script&#xff0c;进行设置&#xff1a; """Author : A Tim…

探索图像处理的利器——OpenCV

目录 引言&#xff1a; 一、OpenCV简介&#xff1a; 二、OpenCV的特点&#xff1a; 三、OpenCV的应用领域&#xff1a; 四、实际案例&#xff1a; 结论&#xff1a; 引言&#xff1a; 在当今信息化的时代&#xff0c;图像处理已经成为了日常生活中不可或缺的一部分。从社…