1. Futex同步机制简介
Futex是fast userspace mutex的缩写,意思是快速用户空间互斥体。它由Hubertus Franke,Matthew Kirkwood,Ingo Molnar和Rusty Russell设计并维护。我们这里讨论Futex是因为在Android中不但线程函数中使用到了Futex,甚至一些模块中在直接使用Futex作为进程间同步的手段,了解Futex的原理将有助于我们更深入的理解这些模块的运行机制。
Linux从2.5.7开始支持Futex。在Unix系统中,传统的进程间同步机制都是通过对内核对象操作来完成的,这个内核对象在需要同步的进程中都是可见的,进程间的同步是通过系统调用在内核中完成。这种同步方式因为涉及用户态和内核态的切换,效率比较低。而且只要使用了传统的同步机制,进入临界区时即使没有其他的进程竞争也必须切换到内核态来检查内核同步对象的状态,这种不必要的切换显然带来了大量的浪费。
Futex就是为了解决这个问题而诞生的。Futex是一种用户态和内核态混合的同步机制,使用Futex同步机制,如果用于进程间同步,需要先调用mmap创建一块共享内存,Futex变量就位于共享区。同时对Futex变量的操作必须是原子的,当进程试图进入临界区或者退出临界区的时候,首先检查共享内存中的Futex变量,如果没有其他的进程也申请使用临界区,则只修改Futex变量而不再执行系统调用。如果同时有其他进程在申请使用临界区,还是需要通过系统调用去执行等待或唤醒操作。这样通过用户态的Futex变量的控制,减少了进程在用户态和内核态之间切换的次数,从而减少了系统同步的开销。
2. Futex用户态操作
Futex的系统调用FUTEX_WAIT和FUTEX_WAKE只是用来挂起或者唤醒进程,Futex的同步机制还包括用户态下的判断操作。用户态下的操作没有固定的函数调用,只是一种检测共享变量的方法。下面将介绍如何将Futex用于临界区。
首先需要创建一个整型计数器作为Futex变量,如果是进程间同步,这个变量必须位于共享内存。Futex变量的初始值为0。
当进程或线程尝试持有锁的时候,检查Futex变量的值是否为0。如果是,则将Futex变量的值设为1以后再继续,如果不是,将Futex变量的值设为2以后再执行FUTEX_WAIT()的系统调用进入等待。
前面的值0表示无锁的状态,1表示有锁无竞争的状态,2表示有竞争的状态。
当进程或线程释放锁的时候,如果Futex变量的值为1,说明没有其他的线程在等待锁,这样把Futex变量的值设为0就结束了。如果Futex变量的值为2,说明还有线程在等待锁,将Futex变量的值设为0,同时还需要执行FUTEX_WAKE()系统调用来唤醒等待的进程。
在对Futex变量操作时,比较和赋值操作必须是原子的。Bionic提供了用于这种原子操作的函数__bionic_cmpxchg(),这个函数的原型如下:
int __bionic_cmpxchg(int32_t old_value, int32_t new_value, volatile int32_t* ptr)
这个函数会比较ptr地址中的值和old_value的值,相等则将new_value赋予ptr指向的地址。函数如果成功则返回0,否则返回非0。
3. Futex系统调用
在Linux中,Futex系统调用的定义如下
#define __NR_futex 240
3.1. Futex系统调用的用法为:
int futex (int *uaddr, int op, int val, const struct timespec *timeout, int *uaddr2, int val3);
uaddr是Futex变量,一个共享的整型计数器。
op表示操作类型,有五种预定义的值,但是在Binoc中只使用了下面两种:
1) FUTEX_WAIT: 内核将检查uaddr中计数器的值是否等于val,如果等于则挂起进程,直到在uaddr上到来了FUTEX_WAKE调用或者超时时间到。
2) FUTEX_WAKE: 内核唤醒val个等待在uaddr上的进程。
val存放与操作op相关的值。
timeout用作操作FUTEX_WAIT中,表示等待超时时间。
uAddr2和val3很少使用。
3.2. 在Bionc中,提供了两个函数来包装Futex系统调用
extern int __futex_wait(volatile void *ftx, int val, const struct timespec *timeout); extern int __futex_wake(volatile void *ftx, int count);
Bionc中还有两个类似的函数,它们的原型如下:
extern int __futex_wake_ex(volatile void *ftx, int pshared, int val); extern int __futex_wait_ex(volatile void *ftx, int pshared, int val, const struct timespec *timeout);
这两个函数比前面的多了一个参数pshared。pshared的值为true表示wake和wait操作是用于进程间的挂进和唤醒;值为false表示操作用于进程内线程的挂进和唤醒。当pshared的值为false时,执行Futex系统调用的操作码为:
FUTEX_WAIT|FUTEX_PRIVATE_FLAG,
FUTEX_WAKE|FUTEX_PRIVATE_FLAG
这样内核如果检测到操作有 FUTEX_PRIVATE_FLAG 标记,能以更快的速度执行挂起和唤醒操作。
__futex_wait 和 __futex_wake函数相当于pshared等于true的情况。
3.3. Android中有些模块还会使用下面的Futex函数:
extern int __futex_syscall3(volatile void *ftx, int op, int val); extern int __futex_syscall4(volatile void *ftx, int op, int val, const struct timespec *timeout);
__futex_syscall3()相当于__futex_wake(),而__futex_syscall4()相当于__futex_wait()。这两个函数与前面的区别是能指定操作码op做为参数。操作码可以是FUTEX_WAIT,FUTEX_WAKE或者它们和FUTEX_PRIVATE_FLAG的组合。
Android中操作码的定义如下:
#define FUTEX_WAIT 0 #define FUTEX_WAKE 1 #ifndef FUTEX_PRIVATE_FLAG #define FUTEX_PRIVATE_FLAG 128 #endif #ifndef FUTEX_WAIT_PRIVATE #define FUTEX_WAIT_PRIVATE (FUTEX_WAIT|FUTEX_PRIVATE_FLAG) #endif #ifndef FUTEX_WAKE_PRIVATE #define FUTEX_WAKE_PRIVATE (FUTEX_WAKE|FUTEX_PRIVATE_FLAG) #endif
4. 使用例子
/* * This sample show how to use futex betwen two process, and use system v * shared memory to store data */ #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ipc.h> #include <sys/mman.h> #include <sys/types.h> #include <sys/syscall.h> #include <sys/wait.h> #include <sys/stat.h> #include <fcntl.h> #include <errno.h> #if __GLIBC_PREREQ(2, 3) #if defined FUTEX_WAIT || defined FUTEX_WAKE #include <linux/futex.h> #else #define FUTEX_WAIT 0 #define FUTEX_WAKE 1 #endif #ifndef __NR_futex #define __NR_futex 202 /* syscall number */ #endif #endif #define FILE_MODE (S_IRUSR | S_IWUSR) const char shmfile[] = "/tmp"; //if change to "./tmp", report error! const int size = 100; struct namelist { int id; char name[20]; }; int main(void) { int fd, pid, status; int *ptr; struct stat stat; // create a Posix shared memory int flags = O_RDWR | O_CREAT; fd = shm_open(shmfile, flags, FILE_MODE); if (fd < 0) { printf("shm_open failed, errormsg=%s errno=%d ", strerror(errno), errno); return 0; } ftruncate(fd, size); ptr = (int *)mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); pid = fork(); if (pid == 0) { // child process sleep(2); printf("Child %d: start ", getpid()); fd = shm_open(shmfile, flags, FILE_MODE); fstat(fd, &stat); ptr = (int *)mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); close(fd); struct namelist tmp; // store total num in ptr[0]; *ptr = 3; struct namelist *cur = (struct namelist *)(ptr+1); // store items tmp.id = 1; strcpy(tmp.name, "Nellson"); *cur++ = tmp; tmp.id = 2; strcpy(tmp.name, "Daisy"); *cur++ = tmp; tmp.id = 3; strcpy(tmp.name, "Robbie"); *cur++ = tmp; printf("wake up parent "); syscall(__NR_futex, ptr, FUTEX_WAKE, 1, NULL); exit(0); } else{ // parent process int i; printf("parent start waiting "); syscall(__NR_futex, ptr, FUTEX_WAIT, *(int *)ptr, NULL); printf("parent end waiting "); struct namelist tmp; int total = *ptr; printf(" There is %d item in the shm ", total); ptr++; struct namelist *cur = (struct namelist *)ptr; for (i = 0; i < total; i++) { tmp = *cur; printf("%d: %s ", tmp.id, tmp.name); cur++; } printf(" "); waitpid(pid, &status, 0); } // remvoe a Posix shared memory from system printf("Parent %d get child status:%d ", getpid(), status); return 0; } /* root@ubuntu:/work/8.futex_test# gcc futex_test.c -o pp -lrt root@ubuntu:/work/8.futex_test# ./pp parent start waiting Child 21650: start wake up parent parent end waiting There is 3 item in the shm 1: Nellson 2: Daisy 3: Robbie Parent 21649 get child status:0 */
5. 补充
1. kernel ools estingselftestsfutex 有对futex的测试代码
2. 系统调用futex实现
//kernelfutex.c SYSCALL_DEFINE6(futex, u32 __user *, uaddr, int, op, u32, val, struct timespec __user *, utime, u32 __user *, uaddr2, u32, val3)
参考:
A futex overview and update:https://lwn.net/Articles/360699/
http://blog.sina.com.cn/s/blog_dae890d10101f01l.html
http://blog.sina.com.cn/s/blog_dae890d10101f01i.html
http://blog.sina.com.cn/s/blog_dae890d10101f01q.html