知识点总结

1:Linux设备中的并发控制
1.1:原子操作:
atomic_t v = ATOMIC_INIT(0);
atomic_read(atomic_t *v);
atomic_add(int i, atomic_t *v);
atomic_sub(int i, atomic_t *v);

1.2:自旋锁:
spinlock_t lock;
spin_lock_init(lock);
spin_lock(lock);
spin_unlock(lock);
spin_trylock(lock);
spin_lock_irq() = spin_lock() + local_irq_disable()
spin_lock_irqsave() = spin(lock) + local_irq_save()
spin_lock_bh() = spin_lock() + local_bh_disable()
对于单CPU和内核不支持抢占,自旋锁退化为空操作;
对于单CPU和内核可抢占,自旋锁期间关中断,抢占被禁止;
对于多核SMP,拥有自旋锁的核抢占被禁止,不影响其它核的抢占调度;
ARM平台处理器通过LDREX、STREX实现:
LDREX用来读取内存中的值,并标记对该段内存的独占访问;
STREX在更新内存数值时,会检查该段内存是否已经被标记为独占访问,并以此来决定是否更新内存中的值。
注意:
(1):自旋锁实际上是忙等待,只有再占用锁的时间极短的情况下,使用自旋锁才合理。
(2):自旋锁可能导致系统死锁,引发这个问题最常见的情况是递归使用一个自旋锁。
(3):在自旋锁锁定期间不能调用可能引起进程调度的函数,如kmalloc(),可能导致内核奔溃。
(4):在单核情况下编程,也应该认为自己和CPU是多核的,强调跨平台的概念。

1.3:信号量:
struct semaphore sem;
sema_init(struct semaphore *sem, int val);
down(struct semaphore *sem);
up(struct semaphore *sem);
down_interruptible(struct semaphore *sem);
down_trylock(struct semaphore *sem);
注意:
信号量的使用方式与自旋锁类似,但区别是当获取不到信号量时,进程进入休眠等待状态。

1.4:互斥体
struct mutex my_mutex;
mutex_init(&my_mutex);
mutex_lock(&my_mutex);
mutex_unlock(&my_mutex);
mutex_lock_interruptible(&my_mutex);
mutex_trylock(&my_mutex);
严格意义上讲,互斥体和自旋锁属于不同层次的互斥手段,前者的实现依赖于后者。在互斥体本身实现上,为了保证互斥体结构存取的原子性,需要自旋锁来互斥。所以自旋锁属于更底层的手段。
注意:
(1):当锁不能被获取到时,使用互斥体的开销是进程上下文切换时间,使用自旋锁开销是等待获取自旋锁。
(2):互斥体所保护的临界区可包含可能引起阻塞的代码,而自旋锁则绝对要避免这种情况。
(3):互斥体存在于进程上下文,因此如果被保护的共享资源需要在中断或者软中断种使用,则在互斥体和自旋锁种只能选择自旋锁。

1.5:完成量
struct completion my_completion;
init_completion(&my_completion);
wait_for_completion(&my_completion);
complete(&my_completion);

2:Linux设备驱动中的阻塞与非阻塞IO
2.1:等待队列:
wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);
wait_event(my_queue, condition);
wait_event_interruptible(my_queue, condition);
wait_event_timeout(my_queue, condition, timeout);
wake_up(&my_queue);

2.2:轮询操作:
2.2.1:应用程序中最广泛用到的是BSD UNIX中引入的select()系统调用:
int select(int numfds, fs_set *readfds, fd_set *writefds, fs_set *exceptfds, struct timeval *timeout);
第一次对n个文件进行select()的时候,若任何一个文件满足要求,select()就直接返回;第二次再进行select()的时候,若没有文件满足读写要求,select()的进程阻塞且休眠。由于调用select()的时候,每个驱动的poll()接口都会被调用到,实际上执行select()的进程被挂到了每个驱动的等待队列上,可以被任何一个驱动唤醒。
FD_ZERO(fd_set *set);
FD_SET(int fd, fd_set *set);
FD_CLR(int fd, fd_set *set);
FD_ISSET(int fd, fd_set *set);
2.2.2:设备驱动中的poll()函数的原型是:
unsigned int (*poll)(struct file *filp, struct poll_table *wait);
该函数进行两项工作:
(1):对可能引起设备文件状态变化的等待队列调用poll_wait()函数,将对应的等待队列头部添加到poll_table中。
(2):返回表示是否能对设备进行无阻塞读、写访问的掩码。
void poll_wait(struct file *filp, wait_queue_head_t *queue, poll_table *wait);
poll_wait()这个函数并不会阻塞,它所作的工作是把当前进程添加到wait参数指定的等待列表(poll_table)中,实际作用是让唤醒参数queue对应的等待队列可以唤醒因select()而睡眠的进程。
poll()函数的典型模板:
static unsigned int xxx_poll(struct file *filp, poll_table *wait)
{
unsigned int mask = 0;
struct xxx_dev *dev = filp->private_data;

poll_wait(filp, &dev->r_wait, wait);
poll_wait(filp, &dev->w_wait, wait);
if(…)
mask |= POLLIN | POLLRDNORM;
if(…)
mask |= POLLOUT | POLLWRNORM;

return mask;
}

3:中断与时钟
3.1:中断类型:
在ARM多核处理器里最常用的中断控制器是GIC,它支持三种类型的中断:
(1):SGI:软件产生的中断,可用于多核的核间通信,多核调度等;
(2):PPI:某个CPU私有外设的中断,这类外设的中断只能发给绑定的那个CPU;
(3):SPI:共享外设的中断,这类外设的中断可以路由到任何一个CPU。

3.2:Linux中断编程:
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);
int devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler, unsigned long irqflags, const char *devname, void *dev_id);
devm开头的API申请的是内核“managed”的资源,一般不需要在出错处理和remove()接口里再显示的释放。
void free_irq(unsigned int irq, void *dev_id);
void enable_irq(int irq);
void disable_irq(int irq);
void disable_irq_nosync(int irq);
disable_irq_nosync()和disable_irq()的区别在于前者立即返回,而后者等待目前的中断处理完成。由于disable_irq()会等待指定的中断被处理完成,因此如果在n号中断的顶半部调用disable_irq(n),会引起系统死锁。

3.3:底半部机制
3.3.1:tasklet:它的执行上下文是软中断
void my_tasklet_func(unsigned long);
DECLARE_TASKLET(my_tasklet, my_tasklet_func, data);
tasklet_schedule(&my_tasklet);
注意:
(1): 在硬件中断A的底半部处理过程中,又产生了中断A,这种情况下A的上半部代码执行两次,底半部代码只执行一次,所以同一个中断的上半部和底半部,在执行时是多对一的关系。
(2):在硬件中断A的底半部处理过程种,又产生了中断B,这种情况下,在处理完B的上半部后,会继续处理A的底半部,也会去执行B的底半部,所以多个中断的底半部,是汇集在一起处理的。
3.3.2:工作队列:它的执行上下文是内核线程,因此可以调度和休眠
struct work_struct my_wq;
void my_wq_func(struct work_struct *work);
INIT_WORK(&my_w, my_wq_func);
schedule_work(&my_wq);

3.4:threaded_irq
int devm_request_threaded_irq(struct device *dev, unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long irqflags, const char *devname, void *dev_id);
参数handler对应的函数执行于中断上下文,thread_fn参数对应的函数执行于内核线程。如果handler结束的时候,返回值是IRQ_WAKE_THREAD,则内核会调度对应线程执行thread_fn对应的函数。
该函数支持在irqflags种设置IRQF_ONESHOT标记,这样内核会自动帮助我们在中断上下文屏蔽对应的中断号,而在内核调度thread_fn执行后重新使能该中断号。对于我们无法在上半部清除中断的情况,IRQF_ONESHOT特别有用,避免了终端服务程序一退出,中断就洪泛的情况。

3.5:中断共享
(1):共享中断的多个设备在申请中断时,都应该使用IRQF_SHARED标志,而且一个设备以申请某中断成功的前提是该中断未被申请,或者某个中断虽然被申请了,但是之前申请该中断的所有设备也都以IRQF_SHARED标志申请该中断。
(2):尽管内核模块可访问的全局地址都可以作为request_irq(…, void *dev)的最后一个参数dev_id,但是设备结构体指针显然是可传入的最佳参数。
(3):在中断到来时,会遍历执行共享此中断的所有中断处理程序,直到某一个返回IRQ_HANDLED。在中断处理程序顶半部,应根据硬件寄存器的信息比照传入的dev_id参数迅速判断是否为本设备的中断,若不是,应迅速返回IRQ_NONE。

3.6:内核定时器
软件意义上的定时器最终依赖硬件定时器来实现,内核在时钟中断发生后检测各定时器是否到期,到期后的定时器处理函数将作为软中断在底半部执行。实质上,时钟中断处理函数会唤起TIMER_SOFTIRQ软中断,运行当前处理器上到期的所有定时器。
struct timer_list my_timer;
init_timer(&my_timer);
add_timer(&my_timer);
del_timer(&my_timer);

3.7:内核延时
短延迟:忙等待
ndelay();
mdelay();
udelay();
长延迟:忙等待
timer_after();
timer_before();
睡着延迟:
schedule_timeout();
msleep();
sleep();

4:设备I/O端口和I/O内存的访问
设备通常会提供一组寄存器来控制设备、读写设备和获取设备状态,即控制寄存器、数据寄存器和状态寄存器。这些寄存器可能位于I/O空间中,也可能位于内存空间中。当位于内I/O空间时,通常称为I/O端口;当位于内存空间时,对应的内存空间被称为I/O内存。
4.1:I/O端口
unsigned inb(w/l)(unsigned port);
void outb(w/l)(unsigned char(short/long) btye, unsigned port);
void insb(unsigned port, void *addr, unsigned long count);
void outsb(unsigned port, void *addr, unsigned long count);
insb()从端口port开始读count个字节,并将读取结果写入addr指向的内存;outsb()将addr指向的内存中的count个字节连续写入以port开始的端口。

4.2:I/O内存
在内核中访问I/O内存(通常是芯片内部的各个I2C、SPI、USB等控制器的寄存器或者外部内存总线上的设备)之前,需要先使用ioremap()将设备所处的物理地址映射到虚拟地址上。
void *ioremap(unsigned long offset, unsigned long size);
ioremap()与vmalloc()类似,也需要建立新的页表,但是它并不进行vmalloc()中所执行的内存分配行为。ioremap()返回一个特殊的虚拟地址,该地址可用来存取特定的物理地址范围,这个虚拟地址位于vmalloc()映射区域。
readb/w/l
writeb/w/l

4.3:申请与释放
I/O端口:
struct resource *request_region(unsigned long start, unsigned long n, const char *name);
void release_region(unsigned long start, unsigned long n);
访问流程:
request_region()->inb()/outb()等->release_region()
I/O内存:
struct resource *request_mem_region(unsigned long start, unsigned long len, char *name);
void release_mem_region(unsigned long start, unsigned long len);
访问流程:
request_mem_region()->ioremap()->readb/writeb等->iounmap()->release_mem_region()
注意:
request_mem_region仅仅是linux对IO内存的管理,意思指这块内存我已经占用了,别人就不要动了,也不能被swap出去。使用这些寄存器时,可以不调用request_mem_region,但这样的话就不能阻止别人对他的访问了。

5: 进程间通信
由于每个进程的用户空间都是独立的,不能相互访问,这时就需要借助内核空间来实现进程间通信,原因很简单,每个进程都是共享一个内核空间。

Linux 内核提供了不少进程间通信的方式,其中最简单的方式就是管道,管道分为「匿名管道」和「命名管道」。

匿名管道顾名思义,它没有名字标识,匿名管道是特殊文件只存在于内存,没有存在于文件系统中,shell 命令中的「|」竖线就是匿名管道,通信的数据是无格式的流并且大小受限,通信的方式是单向的,数据只能在一个方向上流动,如果要双向通信,需要创建两个管道,再来匿名管道是只能用于存在父子关系的进程间通信,匿名管道的生命周期随着进程创建而建立,随着进程终止而消失。

命名管道突破了匿名管道只能在亲缘关系进程间的通信限制,因为使用命名管道的前提,需要在文件系统创建一个类型为 p 的设备文件,那么毫无关系的进程就可以通过这个设备文件进行通信。另外,不管是匿名管道还是命名管道,进程写入的数据都是缓存在内核中,另一个进程读取数据时候自然也是从内核中获取,同时通信数据都遵循先进先出原则,不支持 lseek 之类的文件定位操作。

消息队列克服了管道通信的数据是无格式的字节流的问题,消息队列实际上是保存在内核的「消息链表」,消息队列的消息体是可以用户自定义的数据类型,发送数据时,会被分成一个一个独立的消息体,当然接收数据时,也要与发送方发送的消息体的数据类型保持一致,这样才能保证读取的数据是正确的。消息队列通信的速度不是最及时的,毕竟每次数据的写入和读取都需要经过用户态与内核态之间的拷贝过程。

共享内存可以解决消息队列通信中用户态与内核态之间数据拷贝过程带来的开销,它直接分配一个共享空间,每个进程都可以直接访问,就像访问进程自己的空间一样快捷方便,不需要陷入内核态或者系统调用,大大提高了通信的速度,享有最快的进程间通信方式之名。但是便捷高效的共享内存通信,带来新的问题,多进程竞争同个共享资源会造成数据的错乱。

那么,就需要信号量来保护共享资源,以确保任何时刻只能有一个进程访问共享资源,这种方式就是互斥访问。信号量不仅可以实现访问的互斥性,还可以实现进程间的同步,信号量其实是一个计数器,表示的是资源个数,其值可以通过两个原子操作来控制,分别是 P 操作和 V 操作。

与信号量名字很相似的叫信号,它俩名字虽然相似,但功能一点儿都不一样。信号是进程间通信机制中唯一的异步通信机制,信号可以在应用进程和内核之间直接交互,内核也可以利用信号来通知用户空间的进程发生了哪些系统事件,信号事件的来源主要有硬件来源(如键盘 Cltr+C )和软件来源(如 kill 命令),一旦有信号发生,进程有三种方式响应信号 1. 执行默认操作、2. 捕捉信号、3. 忽略信号。有两个信号是应用进程无法捕捉和忽略的,即 SIGKILL 和 SEGSTOP,这是为了方便我们能在任何时候结束或停止某个进程。

前面说到的通信机制,都是工作于同一台主机,如果要与不同主机的进程间通信,那么就需要 Socket 通信了。Socket 实际上不仅用于不同的主机进程间通信,还可以用于本地主机进程间通信,可根据创建 Socket 的类型不同,分为三种常见的通信方式,一个是基于 TCP 协议的通信方式,一个是基于 UDP 协议的通信方式,一个是本地进程间通信方式。

以上,就是进程间通信的主要机制了。你可能会问了,那线程通信间的方式呢?

同个进程下的线程之间都是共享进程的资源,只要是共享变量都可以做到线程间通信,比如全局变量,所以对于线程间关注的不是通信方式,而是关注多线程竞争共享资源的问题,信号量也同样可以在线程间实现互斥与同步:

互斥的方式,可保证任意时刻只有一个线程访问共享资源;

同步的方式,可保证线程 A 应在线程 B 之前执行;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值