现代操作系统有三大特性:中断处理、多任务处理和多处理器。这些特性导致当多个进程、线程或者CPU同时访问一个资源时,可能发生错误,这些错误是操作系统运行所不允许的。在操作系统中,内核需要提供并发控制机制,对共享资源进行保护。
在操作系统中,并发是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。并发容易导致竞争的问题。竞争就是两个或两个以上的进程同时访问一个资源,同时引起资源的错误。并发控制机制有以下几种:
1.原子变量操作:
原子变量操作(分为原子整型操作和原子位操作)就是绝不会在执行完毕前被任何其他任务和时间打断,不会执行一半,又去执行其他代码。原子操作需要硬件的支持,因此是架构相关的,其API和原子类型的定义都在include/asm/atomic.h中,使用汇编语言实现。
在linux中,原子变量的定义如下:
typedef struct {
volatile int counter;
} atomic_t;
关键字volatile用来暗示GCC不要对该类型做数据优化,所以对这个变量counte的访问都是基于内存的,不要将其缓冲到寄存器中。存储到寄存器中,可能导致内存中的数据已经改变,而寄存其中的数据没有改变。
(1)原子整型操作:
1)定义atomic_t变量:
#define ATOMIC_INIT(i) ( (atomic_t) { (i) } )
atomic_t v = ATOMIC_INIT(0); //定义原子变量v并初始化为0
2)设置原子变量的值:
#define atomic_set(v,i) ((v)->counter = (i))
void atomic_set(atomic_t *v, int i);//设置原子变量的值为i
3)获取原子变量的值:
#define atomic_read(v) ((v)->counter + 0)
atomic_read(atomic_t *v);//返回原子变量的值
4)原子变量加/减:
static __inline__ void atomic_add(int i, atomic_t * v); //原子变量增加i
static __inline__ void atomic_sub(int i, atomic_t * v); //原子变量减少i
5)原子变量自增/自减:
#define atomic_inc(v) atomic_add(1, v); //原子变量加1
#define atomic_dec(v) atomic_sub(1, v); //原子变量减1
6)操作并测试:
//这些操作对原子变量执行自增,自减,减操作后测试是否为0,是返回true,否则返回false
#define atomic_inc_and_test(v) (atomic_add_return(1, (v)) == 0)
static inline int atomic_add_return(int i, atomic_t *v)
(2)原子位操作(根据数据的每一位单独进行操作):
View Code
原子操作的优点编写简单;缺点是功能太简单,只能做计数操作,保护的东西太少。
2.自旋锁
自旋锁是专为防止多处理器并发而引入的一种锁,它应用于中断处理等部分。对于单处理器来说,防止中断处理中的并发可简单采用关闭中断的方式,不需要自旋锁。
自旋锁最多只能被一个内核任务持有,如果一个内核任务试图请求一个已被争用(已经被持有)的自旋锁,那么这个任务就会一直进行忙循环——旋转——等待锁重新可用。要是锁未被争用,请求它的内核任务便能立刻得到它并且继续进行。自旋锁可以在任何时刻防止多于一个的内核任务同时进入临界区,因此这种锁可有效地避免多处理器上并发运行的内核任务竞争共享资源。
(1)自旋锁的使用:
spinlock_t spin; //定义自旋锁
spin_lock_init(lock); //初始化自旋锁
spin_lock(lock); //成功获得自旋锁立即返回,否则自旋在那里直到该自旋锁的保持者释放
spin_trylock(lock); //成功获得自旋锁立即返回真,否则返回假,而不是像上一个那样"在原地打转"
spin_unlock(lock);//释放自旋锁
使用代码:
spinlock_t lock;
spin_lock_init(&lock);
spin_lock (&lock);
....//临界资源区
spin_unlock(&lock);
(2)注意事项:
1)自旋锁是一种忙等待。它是一种适合短时间锁定的轻量级的加锁机制。
2)自旋锁不能递归使用。自旋锁被设计成在不同线程或者函数之间同步。这是因为,如果一个线程在已经持有自旋锁时,其处于忙等待状态,则已经没有机会释放自己持有的锁了。如果这时再调用自身,则自旋锁永远没有执行的机会了。
3.信号量
linux中,提供了两种信号量:一种用于内核程序中,一种用于应用程序中。这里只讲属前者。
信号量和自旋锁的使用方法基本一样。与自旋锁相比,信号量只有当得到信号量的进程或者线程时才能够进入临界区,执行临界代码。信号量和自旋锁的最大区别在于:当一个进程试图去获得一个已经锁定的信号量时,进程不会像自旋锁一样在远处忙等待。
信号量是一种睡眠锁。如果有一个任务试图获得一个已被持有的信号量时,信号量会将其推入等待队列,然后让其睡眠。这时处理器获得自由去执行其它代码。当持有信号量的进程将信号量释放后,在等待队列中的一个任务将被唤醒,从而便可以获得这个信号量。
(1)信号量的实现:
在linux中,信号量的定义如下:
struct semaphore {
spinlock_t lock; //用来对count变量起保护作用。
unsigned int count; // 大于0,资源空闲;等于0,资源忙,但没有进程等待这个保护的资源;小于0,资源不可用,并至少有一个进程等待资源。
struct list_head wait_list; //存放等待队列链表的地址,当前等待资源的所有睡眠进程都会放在这个链表中。
};
(2)信号量的使用:
1)定义信号量:
struct semaphore sem;
2)初始化信号量 :
static inline void sema_init(struct semaphore *sem, int val); //设置sem为val
#define init_MUTEX(sem) sema_init(sem, 1) //初始化一个用户互斥的信号量sem设置为1
#define init_MUTEX_LOCKED(sem) sema_init(sem, 0) //初始化一个用户互斥的信号量sem设置为0
定义和初始化可以一步完成:
DECLARE_MUTEX(name); //该宏定义信号量name并初始化1
DECLARE_MUTEX_LOCKED(name); //该宏定义信号量name并初始化0
当信号量用于互斥时(即避免多个进程同是在一个临界区运行),信号量的值应初始化为1。这种信号量在任何给定时刻只能由单个进程或线程拥有。在这种使用模式下,一个信号量有事也称为一个“互斥体(mutex)”,它是互斥(mutual exclusion)的简称。Linux内核中几乎所有的信号量均用于互斥。
使用信号量,内核代码必须包含<asm/semaphore.h> 。
3)获取(锁定)信号量:
void down(struct semaphore *sem);
View Code
int down_interruptible(struct semaphore *sem);
View Code
int down_killable(struct semaphore *sem);
View Code
4)释放信号量
void up(struct semaphore *sem); //释放信号量sem,唤醒等待者
4.完成量
它用于一个执行单元等待另一个执行单元执行完某事;
struct completion {
unsigned int done; //大于0,表示完成量的函数可以立即执行,不要要等待;等于0,将拥有完成量的线程置于等待状态。
wait_queue_head_t wait;
};
使用方法:
1)定义完成量:
struct completion com;
2)初始化:
init_completion(&com); //要是觉得这两步麻烦,就再给你来个宏即定义又初始化DECLARE_COMPLETION(com);
3)等待完成量:
void __sched wait_for_completion(struct completion *x); //等待一个completion被唤醒
View Code
int __sched wait_for_completion_interruptible(struct completion *x);//可中断的wait_for_completion
View Code
unsigned long __sched wait_for_completion_timeout(struct completion *x, unsigned long timeout);//带超时处理的wait_for_completion
View Code
4)唤醒完成量
void complete(struct completion *x); //只唤醒一个等待的进程或线程。
void complete_all(struct completion *x); //唤醒所有等待这个完成量的进程或者线程。
后记:除了上述几种广泛使用的的并发控制机制外,还有中断屏蔽、顺序锁(seqlock)、RCU(Read-Copy-Update)等等,做个简单总结如下图:
相关推荐
在Linux操作系统中,多线程并发控制是一个至关重要的主题,特别是在设备驱动编程和系统级开发中。本文将深入探讨Linux下并发控制的原因、方法以及具体的实现机制。 并发控制的主要原因是由于多个线程可能同时访问...
1. **网络通信基础**:在“并发回音服务器”中,我们主要关注的是TCP(传输控制协议)或UDP(用户数据报协议)。TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议,适合需要稳定、无丢失的数据传输场景。而...
"牛客网Linux高并发服务器开发"这个压缩包文件,显然聚焦于利用Linux进行高性能服务器的设计与实现,这涉及到多个核心知识点。下面将详细阐述这些关键概念。 1. **嵌入式Linux开发**: 嵌入式Linux是Linux操作系统...
在驱动程序中,当多个线程同时访问相同的资源时,可能会引发\"竞态\",因此我们必须对共享资源进行并发控制...Linux内核中解决并发控制的最常用方法是自旋锁与信号量。本文详细介绍了自旋锁与信号量的特点和实现方法。
Linux 驱动并发控制之位原子操作 本文主要讲述 Linux 驱动并发控制中的位原子操作,包括其原理、常用的位原子操作函数、设备注册、驱动源码文件等。 一、位原子操作的原理 位原子操作是利用位操作来实现并发控制...
在Linux设备驱动开发中,并发控制是一个至关重要的概念,它涉及到多线程、中断处理以及系统资源的高效安全共享。《Linux设备驱动开发详解》一书由宋宝华撰写,人民邮电出版社出版,深入浅出地介绍了这一主题。本文将...
【Linux的多进程并发控制设计与实现】 在Linux操作系统中,多进程并发控制是系统设计中的关键要素,尤其是在处理大规模并发请求的场景下,如证券交易系统。本课题以证券交易系统为背景,探讨如何在Linux环境下设计...
在构建高性能的Linux服务器以处理高并发场景时,结合C语言编程和MySQL数据库操作是常见的技术选择。这里我们将深入探讨如何利用Linux系统特性、C语言编程技巧以及优化SQL查询来实现这一目标。 首先,理解Linux操作...
本资料“Linux设备驱动中的并发控制”深入探讨了这一主题。 并发控制的目标是防止数据不一致性和资源争用,以确保系统在并发执行时仍能保持其正确性。在Linux设备驱动中,这通常涉及到锁、信号量、原子操作以及中断...
在Linux系统中,多线程并发控制是保证系统稳定性和数据一致性的重要机制。本文主要讨论了在Linux环境下,如何管理和控制多线程并发访问共享资源,以防止竞态条件和其他同步问题的发生。以下是对相关知识点的详细阐述...
它利用Linux内核的epoll事件模型来实现高效的并发控制,确保在大量用户访问时保持系统的稳定性和效率。本文旨在探讨Nginx如何利用Linux系统特性实现高并发控制,并对比传统的多进程并发模型和I/O多路复用模型。 ...
总的来说,这个示例程序涵盖了Linux系统编程中的重要概念,如GUI编程、并发控制和进程管理,是学习和理解这些主题的好例子。通过分析和理解这个程序,开发者可以提升在Linux环境下构建复杂应用的能力。
在Linux中,可以采用多种机制来实现进程并发控制,如信号量、互斥锁、条件变量等。 在证券交易系统的设计中,通常会将应用程序和通信功能进行分离,客户端和服务器端通过特定的通信规则交互。客户端负责用户界面和...
【进程控制实验】是Linux操作系统课程中的一个重要环节,旨在帮助学生深入理解进程的概念以及进程间的交互。通过实验,学生能够掌握以下关键知识点: 1. **进程的概念与程序的区别**: 进程是操作系统资源分配的...
在Linux系统中,服务器并发能力的提升是通过各种优化手段实现的。Linux的内核优化是一个复杂且效果显著的方法,尤其在硬件资源有限的情况下,通过调整内核参数来提高并发处理能力,能够有效提高服务器性能。服务器...
C++学习笔记和实践项目,实践项目包括Json工具类、设计模式的C++实现、消息队列、智能指针,linux下的并发控制工具、线程池,epoll管理器和Mysql连接池、STL容器的快捷输出工具和页面置换算法(FIFO, LRU, LFU)的...
增加并发控制后的globalmem程序
"Linux测试使用Shell并发脚本"这个主题涉及到的是如何利用Shell脚本来实现多任务并行执行,提高工作效率。在实际工作中,这样的脚本对于进行性能测试、自动化测试等场景尤其有用。 首先,我们来理解一下Shell脚本的...