【1.2】同步的方法(内核同步,线程同步,进程通信)

一、内核同步

主要是防止多核处理器同时访问修改某段代码,或者在对设备驱动程序进行临界区保护。主要有以下几种方式:

1、原子操作:原子操作可以保证指令以原子的方式执行,执行过程不被打断。

atomic_t u = ATOMIC_INIT(0);// 定义原子变量u并初始化0
atomic_set(&u,4);
atomic_add(2,&u);
atomic_inc(&u);
atomic_read(&u);

2、自旋锁:自旋锁最多只能被一个可执行线程持有。若未获得锁,该线程会一直进行忙循环等待,不能睡眠(自旋锁禁止处理器抢占)。自旋锁不可递归!

DEFINE_SPINLOCK(mr_lock);
unsigned long flags

/*自旋锁实现*/
spin_lock(&mr_lock);

spin_unlock(&mr_lock);

/*在中断处理程序中使用自旋锁*/
spin_lock_irqsave(&mr_lock,flags); //保存中断的当前状态,并禁止本地中断(防止中断触发,双重请求触发死锁)

spin_unlock_irqestore(&mr_lock,flags); //让中断恢复加锁前状态

3、读写自旋锁:多个任务可以并发持有读者锁,而写锁只能被一个任务持有,并且不能并发读操作。

DEFINE_RWLOCK(mr_rwlock);

read_lock(&mr_rwlock);//等待写者释放锁;
read_unlcok(&mr_rwlock);

write_lock(&mr_rwlock);//等待读者释放锁;
write_unlcok(&mr_rwlock);

4、信号量:信号量是一种睡眠锁,允许多个任务(信号量的数量)在同一时刻访问同一资源,未获得锁的任务被推进等待队列进入睡眠。比自旋锁有更高CPU利用率,但是开销大。

struct semaphore mr_sem;
sema_init(&mr_sem,count)//count是信号量的使用数量;

if(down_interruptible(&mr_sem)) //down()会让进程进入TASK_UNINTERRUPTIBLE状态,进程不再响应信号(非信号量),只能等待wake_up。
{}
up(&mr_sem);

5、互斥体:相当于允许睡眠的自旋锁,其与计数为1的信号量类似,但是操作接口更简单,实现更高效,使用限制强。

  • 只有一个任务可以持有互斥锁。
  • 上锁者必须解锁。不能在上下文中锁定,在另一个上下文解锁。
  • 不能递归使用。
  • 持有锁的进程不能退出。
  • 不能在中断或下半部使用。
  • 只能使用官方API。
DEFINE_MUTEX(mutex);
mutex_init(&mutex);

mutex_lock(&mutex);
mutex_unlock(&mutex);
mutex_trylock(&mutex);//试图获取锁,成功返回1.失败返回0;

6、完成变量:当一个任务完成后,通过完成变量唤醒等待的任务。

DECLARE_COMPLETION(mr_comp);//创建完成变量并初始化

init_completion(&mr_comp);//初始化完成变量
wait_for_completion(&mr_comp);//等待完成变量
complete(&mr_comp);//发信号唤醒等待任务

7、顺序锁:顺序锁对读写锁的一种优化,使用顺序锁时,读不会被写执行单元阻塞(在读写锁中,写操作必须要等所有读操作完成才能进行)。也就是说,当向一个临界资源中写入的同时,也可以从此临界资源中读取,即实现同时读写,但是不允许同时写数据。如果读执行单元在读操作期间,写执行单元已经发生了写操作,那么,读执行单元必须重新开始。

seqlock_t seqlock = DEFINE_SEQLOCK(seqlcok);

write_seqlock(&seqlock);
    write_something();    // 写操作代码块
write_sequnlock(&seqlock);

do{
    seqnum = read_seqbegin(&seqlock);  // 读执行单元在访问共享资源时要调用该函数,返回锁seqlock的顺序号 
    read_something(); // 读操作代码段 
} while( read_seqretry(&seqlock, seqnum)); // 在读结束后调用此函数来检查,是否有写执行单元对资源进行操作,若有则重新读。

8、禁止抢占:可以通过preempt_disable()禁止内核抢占。这是一个可以嵌套调用的函数,可以调用任意次。每次调用都必须有一个相应的preempt_enable()调用。当最后一次preempt_enable()被调用,内核抢占才重新启用。

9、顺序和屏障:可能对读和写重新排序这样无疑使问题复杂化了。所有可能重新排序和写的处理器提供了机器指令来确保顺序要求。同样也可以指示编译器不要对给定点周围的指令序列进行重新排序。这些确保顺序的指令称作屏障(barrirers)。

//线程1           //线程2
a = 3;            —————
mb();             —————
b = 4;            c = b;
—————             rmb();
—————             d = a;     

如果不使用内存屏障,在某些处理器上,c可能接收了b的新值,而d接收了a原来的值。比如c可能等于4(正是我们希望的),然而d可能等于1(不是我们希望的)口使用mb()能确保a和b按照预定的顺序写入,而rmb()确保c和d按照预定的顺序读取。

二、线程同步

1、临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。它并不是核心对象,不是属于操作系统维护的,而是属于进程维护的。

2、互斥对象:互斥对象和临界区很像,采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程同时访问。当前拥有互斥对象的线程处理完任务后必须将线程交出,以便其他线程访问该资源。

3、信号量:信号量也是内核对象。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。在用CreateSemaphore()创建信号量时即要同时指出允许的最大资源计数和当前可用资源计数。一般是将当前可用资源计数设置为最大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就会-1 ,只要当前可用资源计数是大于0 的,就可以发出信号量信号。但是当前可用计数减小到0 时则说明当前占用资源的线程数已经达到了所允许的最大数目,不能在允许其他线程的进入,此时的信号量信号将无法发出。线程在处理完共享资源后,应在离开的同时通过ReleaseSemaphore()函数将当前可用资源计数+1 。

4、事件对象:通过通知操作的方式来保持线程的同步,还可以方便实现对多个线程的优先级比较的操作。首先我们需要创建CreateEvent()一个事件对象,它的使用方式是触发方式,要想被WaitForSingleObject()等待到该事件对象必须是有信号的,事件要想有信号可以用SetEvent()手动置为有信号,要想事件对象无信号可以使用ResetEvent()。

三、进程通信(IPC)

https://www.cnblogs.com/zgq0/p/8780893.html

1、管道:通常指无名管道,是 UNIX 系统IPC最古老的形式。

  • 它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。

  • 它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。

  • 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。

  • 面向字节流,自带同步互斥机制。

1 #include <unistd.h>
2 int pipe(int fd[2]);    // 返回值:若成功返回0,失败返回-1
当一个管道建立时,它会创建两个文件描述符:fd[0]为读而打开,fd[1]为写而打开。如下图:

2、消息队列:在内核中创建一队列,队列中每个元素是一个数据报,不同的进程可以通过句柄去访问这个队列。消息队列提供了⼀个从⼀个进程向另外⼀个进程发送⼀块数据的⽅法。每个数据块都被认为是有⼀个类型值,接收者进程可以根据不同的类型值选择读取。         

  • 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。

  • 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。

  • 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。

#include <sys/msg.h>
// 创建或打开消息队列:成功返回队列ID,失败返回-1
int msgget(key_t key, int flag);
// 添加消息:成功返回0,失败返回-1
int msgsnd(int msqid, const void *ptr, size_t size, int flag);
// 读取消息:成功返回消息数据的长度,失败返回-1
int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
// 控制消息队列:成功返回0,失败返回-1
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

3、信号量:它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。对临界资源进行保护。

P(sv):如果sv的值⼤大于零,就给它减1;如果它的值为零,就挂起该进程的执⾏ 。

V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运⾏,如果没有进程因等待sv⽽挂起,就给它加1。

  • 信号量用于不同进程同步,同一进程实现互斥。

  • 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。

  • 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。

  • 支持信号量组。
#include <sys/sem.h>
// 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
int semget(key_t key, int num_sems, int sem_flags);
// 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
int semop(int semid, struct sembuf semoparray[], size_t numops);  
// 控制信号量的相关信息
int semctl(int semid, int sem_num, int cmd, ...);

4、共享内存: 将同一块物理内存一块映射到不同的进程的虚拟地址空间中,实现不同进程间对同一资源的共享。共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式。

  • 共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。

  • 因为多个进程可以同时操作,所以需要进行同步。信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。

#include <sys/shm.h>
// 创建或获取一个共享内存:成功返回共享内存ID,失败返回-1
int shmget(key_t key, size_t size, int flag);
// 连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1
void *shmat(int shm_id, const void *addr, int flag);
// 断开与共享内存的连接:成功返回0,失败返回-1
int shmdt(void *addr); 
// 控制共享内存的相关信息:成功返回0,失败返回-1
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);

总结

1.管道:速度慢,容量有限,只有父子进程能通讯       

2.消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题    

3.信号量:不能传递复杂消息,只能用来同步    

4.共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值