操作系统-Futex机制

参考链接:

[linux futex浅析] https://developer.aliyun.com/article/6043?spm=a2c6h.13262185.0.0.575c4070sPb94F
[Linux Futex的设计与实现] https://blog.csdn.net/jianchaolv/article/details/7544316#:~:text=Futex%20%E6%98%AFFast%20Userspace%20muTexes%E7%9A%84%E7%BC%A9%E5%86%99%EF%BC%8C%E7%94%B1Hubertus%20Franke%2C%20Matthew%20Kirkwood%2C%20Ingo,%28inter%20process%20communication%29%EF%BC%8C%E5%A6%82%20semaphores%2C%20msgqueues%2C%20sockets%E8%BF%98%E6%9C%89%E6%96%87%E4%BB%B6%E9%94%81%E6%9C%BA%E5%88%B6%20%28flock%20

futex诞生之前

在futex诞生之前,linux下的同步机制可以归为两类:用户态的同步机制 和 内核同步机制。
用户态的同步机制基本上就是利用原子指令实现的spinlock。
内核提供的同步机制,诸如semaphore、等,其实骨子里也是利用原子指令实现的spinlock,内核在此基础上实现了进程的睡眠与唤醒。
使用这样的锁,能很好的支持进程挂起等待。但是最大的缺点是每次lock与unlock都是一次系统调用,即使没有锁冲突,也必须要通过系统调用进入内核之后才能识别。

理想的同步机制应该是在没有锁冲突的情况下在用户态利用原子指令就解决问题,而需要挂起等待时再使用内核提供的系统调用进行睡眠与唤醒。换句话说,用户态的spinlock在trylock失败时,能不能让进程挂起,并且由持有锁的线程在unlock时将其唤醒?

futex来了

现在看来,要实现我们想要的锁,对内核就有两点需求:1、支持一种锁粒度的睡眠与唤醒操作;2、管理进程挂起时的等待队列。
于是futex就诞生了。futex主要有futex_wait和futex_wake两个操作:

// 在uaddr指向的这个锁变量上挂起等待(仅当*uaddr==val时)
int futex_wait(int *uaddr, int val);
// 唤醒n个在uaddr指向的锁变量上挂起等待的进程
int futex_wake(int *uaddr, int n);

内核会动态维护一个跟uaddr指向的锁变量相关的等待队列。
注意futex_wait的第二个参数,由于用户态trylock与调用futex_wait之间存在一个窗口,其间lockval可能发生变化(比如正好有人unlock了)。所以用户态应该将自己看到的*uaddr的值作为第二个参数传递进去,futex_wait真正将进程挂起之前一定得检查lockval是否发生了变化,并且检查过程跟进程挂起的过程得放在同一个临界区中。(参见《linux线程同步浅析》的讨论。)如果futex_wait发现lockval发生了变化,则会立即返回,由用户态继续trylock。

futex实现了锁粒度的等待队列,而这个锁却并不需要事先向内核申明。任何时候,用户态调用futex_wait传入一个uaddr,内核就会维护起与之配对的等待队列。
这件事情听上去好像很复杂,实际上却很简单。其实它并不需要为每一个uaddr单独维护一个队列,futex只维护一个总的队列就行了,所有挂起的进程都放在里面。当然,队列中的节点需要能标识出相应进程在等待的是哪一个uaddr。这样,当用户态调用futex_wake时,只需要遍历这个等待队列,把带有相同uaddr的节点所对应的进程唤醒就行了。
作为优化,futex维护的这个等待队列由若干个带spinlock的链表构成。调用futex_wait挂起的进程,通过其uaddr hash到某一个具体的链表上去。这样一方面能分散对等待队列的竞争、另一方面减小单个队列的长度,便于futex_wake时的查找。每个链表各自持有一把spinlock,将"*uaddr和val的比较操作"与"把进程加入队列的操作"保护在一个临界区中。

另一个问题是关于uaddr参数的比较。futex支持多进程,需要考虑同一个物理内存单元在不同进程中的虚拟地址不同的问题。那么不同进程传递进来的uaddr如何判断它们是否相等,就不是简单数值比较的事情。相同的uaddr不一定代表同一个内存,反之亦然。
两个进程(线程)要想共享同存,无外乎两种方式:通过文件映射(映射真实的文件或内存文件、ipc shmem,以及有亲缘关系的进程通过带MAP_SHARED标记的匿名映射共享内存)、通过匿名内存映射(比如多线程),这也是进程使用内存的唯二方式。
那么futex就应该支持这两种方式下的uaddr比较。匿名映射下,需要比较uaddr所在的地址空间(mm)和uaddr的值本身;文件映射下,需要比较uaddr所在的文件inode和uaddr在该inode中的偏移。注意,上面提到的内存共享方式中,有一种比较特殊:有亲缘关系的进程通过带MAP_SHARED标记的匿名映射共享内存。这种情况下表面上看使用的是匿名映射,但是内核在暗中却会转成到/dev/zero这个特殊文件的文件映射。若非如此,各个进程的地址空间不同,匿名映射下的uaddr永远不可能被futex认为相等。

总结起来说,futex混合用户态和内核态操作,在低并发情况下,减少了系统调用,提高了性能。
在用户态检测到竞争时,需要进入内核态执行挂起到此进程在此共享变量地址上,注意这一步是要同时传地址和当前地址值过去,才能保证期间如果发生了unlock现象,cas操作就操作不成功。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值