0%

最近需要在wsl下启动windows应用,但发现无论执行任何程序,如notepad.exe、powershell.exe均提示permission denied,但我清晰的记得之前微软更新过一个版本,是可以让wsl和windows互通的,而且是比较早的一个特性,谷歌一下搜到了官方的文档:
https://docs.microsoft.com/en-us/windows/wsl/interop

经过一系列排查,才发现之前为了修改wsl的挂载权限,在/etc/wsl.conf中增加了一些配置,导致挂载的时候没有挂载执行权限。原配置:

1
2
3
4
5
6
7
8
[automount]
enabled = true
root = /mnt/
options = "metadata,dmask=022,fmask=133"
mountFsTab = false

[network]
generateResolveConf = true

其中dmask和fmask代表的是挂载的目录权限和文件权限,这个和传统的linux权限(chmod)正好是完全相反的,mask是过滤的意思,比如原来644的权限,对应的mask为777-644=133,因为原来设置的是644权限,所以必然提示permission denied,但是如果改成fmask=033,又会出现所有的文件都有执行权限的问题,所以要想两者兼顾,只能针对盘符进行不同的设置了,比如C盘和D盘一般都是安装的可执行程序,那么就把这两个盘挂载的时候设置为033,E盘和F盘一般为普通文件,就可以设置为133,故只需要在wsl.conf中的mountFsTab设置为true,最终wsl.conf的配置如下:

1
2
3
4
5
6
7
8
[automount]
enabled = true
root = /mnt/
options = "metadata,dmask=022,fmask=133"
mountFsTab = true

[network]
generateResolveConf = true

然后修改/etc/fstab如下:

1
2
3
4
C: /mnt/c drvfs rw,relatime,uid=1000,gid=1000,metadata,umask=22,fmask=033 0 0
D: /mnt/d drvfs rw,relatime,uid=1000,gid=1000,metadata,umask=22,fmask=033 0 0
E: /mnt/e drvfs rw,relatime,uid=1000,gid=1000,metadata,umask=22,fmask=133 0 0
F: /mnt/f drvfs rw,relatime,uid=1000,gid=1000,metadata,umask=22,fmask=133 0 0

至此完美解决。

项目中有这么一个需求,要把本地文件较大批量的拷贝到远程机器,因为拷贝的机器数比较多,所以使用了nohup scp xxx user@host:/path/to/file > /dev/null 2>& &这种异步操作,最近发现经常有一些文件拷贝失败,经排查最终定位到是ssh的连接数超出限制,导致被目标机器拒绝了。

官方文档地址:https://linux.die.net/man/5/sshd_config

其中需要修改的参数是MaxStartups,官方的文档描述如下:

1
2
3
Specifies the maximum number of concurrent unauthenticated connections to the SSH daemon. Additional connections will be dropped until authentication succeeds or the LoginGraceTime expires for a connection. The default is 10.

Alternatively, random early drop can be enabled by specifying the three colon separated values ''start:rate:full'' (e.g. "10:30:60"). sshd(8) will refuse connection attempts with a probability of ''rate/100'' (30%) if there are currently ''start'' (10) unauthenticated connections. The probability increases linearly and all connection attempts are refused if the number of unauthenticated connections reaches ''full'' (60).

简单来说就是这个参数用来指定ssh的最大并发未身份验证连接数,默认是10,在大批量文件拷贝的情况下显然是不够的。

参数可以设置一个整数,如30,也可以设置一个用冒号分割的值:start:rate:full,假如设置的是10:30:60,那么它的作用是:

  • 连接数在10(start)以下不会拒绝连接
  • 当连接数超过10(start)之后,新的连接有30%(rate)的几率被拒绝掉
  • 当连接数超过60(full)后,那么所有的连接均会被拒绝

查到了原因,根据自己实际的情况,将参数设置为合适值,执行service ssh restart重启ssh服务,问题修复。

mmap是POSIX规范接口中用来处理内存映射的一个系统调用,它本身的使用场景非常多,可以用来申请大块内存,可以用来申请共享内存,也可以将文件或设备直接映射到内存中,进程可以像访问普通内存一样访问被映射的文件,在实际开发过程使用场景非常多。这次要聊的重点是mmap映射普通文件的一些原理。

备注:文中引用的所有linux相关源码均基于4.1版本。

1、Linux内存管理

如果要了解mmap的原理,需要先了解一下linux对内存是怎么管理的。
计算机系统中物理内存的大小总是有限的,而我们需要在计算机中运行多个进程,如果允许进程直接访问物理内存,那么可能会发生一些致命的问题,比如A进程不小心把数据写到了B进程的内存中,产生的问题可能是灾难性的。
为了更加安全有效的对内存进行管理,在现代计算机系统中对物理内存做了一层抽象,即虚拟内存。它为每一个进程都提供一块连续的私有地址空间,在32位模式下,每一块虚拟地址空间大小为4GB。
系统将虚拟内存分割成一块块固定大小的虚拟页(Virtual Page),同样的,物理内存也会被分割成物理页(Physical Page),当进程访问内存时,CPU通过内存管理单元(MMU)根据页表(Page Table)将虚拟地址翻译成物理地址,最终取到内存数据。这样在每个进程内部都像是独享整个主存。

而Linux操作系统中,会把高地址的1GB内存作为内核空间,低地址的3GB作为用户空间。
下面是Linux进程内存的布局:

从上图可以看出,一个进程的虚拟空间有多个部分组成,mmap的文件所处的内存空间在内存映射段中。
Linux内核使用vm_area_struct来表示一块内存区域,因为一个进程中会出现多个不同的内存区块,每一个内存块都会使用vm_area_struct结构体来表示,
vm_area_struct的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
struct vm_area_struct {  
/* The first cache line has the info for VMA tree walking. */
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address
within vm_mm. */
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next, *vm_prev;
struct rb_node vm_rb;
unsigned long rb_subtree_gap;

struct mm_struct *vm_mm; /* The address space we belong to. */
pgprot_t vm_page_prot; /* Access permissions of this VMA. */
unsigned long vm_flags; /* Flags, see mm.h. */
struct {
struct rb_node rb;
unsigned long rb_subtree_last;
} shared;

struct list_head anon_vma_chain; /* Serialized by mmap_sem &
* page_table_lock */
struct anon_vma *anon_vma; /* Serialized by page_table_lock */

/* Function pointers to deal with this struct. */
const struct vm_operations_struct *vm_ops;

/* Information about our backing store: */
unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE
units, *not* PAGE_CACHE_SIZE */
struct file * vm_file; /* File we map to (can be NULL). */
void * vm_private_data; /* was vm_pte (shared mem) */
...
};

其中需要重点关注的是vm_ops变量,它指向的是一组函数指针,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct vm_operations_struct {  
void (*open)(struct vm_area_struct * area);
void (*close)(struct vm_area_struct * area);
int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf);
void (*map_pages)(struct vm_area_struct *vma, struct vm_fault *vmf);
int (*page_mkwrite)(struct vm_area_struct *vma, struct vm_fault *vmf);
int (*pfn_mkwrite)(struct vm_area_struct *vma, struct vm_fault *vmf);
int (*access)(struct vm_area_struct *vma, unsigned long addr,
void *buf, int len, int write);
const char *(*name)(struct vm_area_struct *vma);
struct page *(*find_special_page)(struct vm_area_struct *vma,
unsigned long addr);
};

这是一组用于针对不同内存类型的不同操作,可以看到有open(打开)、close(关闭)、fault(缺页异常)等等接口,后面会着重讲针对文件映射的操作原理。
当进程在申请的内存的时候,linux内核其实只分配一块虚拟内存地址,并没有分配实际的物理内存,相当于操作系统只给进程这一块地址的使用权。只有当程序真正使用这块内存时,会产生一个缺页异常,这时内核去真正为进程分配物理页,并建立对应的页表,从而将虚拟内存和物理内存建立一个映射关系,这样可以做到充分利用到物理内存。

2、mmap原理

mmap系统调用的定义:

1
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

参数说明:
start:映射空间的起始地址,一般设置为 NULL;
length:映射空间的长度;
prot:内存保护标志,包括PROT_EXEC(可执行)、PROT_READ(可读)、PROT_WRITE(可写)、PROT_NONE(不可访问) ;
flags:映射类型,通常用来标记共享内存(MAP_SHARED)、匿名映射(MAP_ANONYMOUS)等。
fd:真正要映射的文件描述符;
offset:映射文件的偏移量。

一个简单的demo如下:

1
2
3
4
5
6
7
8
9
10
11
12
int main(int argc, char **argv)  
{
char *filename = "/tmp/foo.data";
struct stat stat;
int fd = open(filename, O_RDWR, 0);
fstat(fd, &stat);
void *bufp = mmap(NULL, stat.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
memcpy(bufp, "Linuxdd", 7);
munmap(bufp, stat.st_size);
close(fd);
return 0;
}

从demo中可以看出,mmap是将一个文件直接映射到进程的地址空间,进程可以像操作内存一样去读写磁盘上的文件内容,而不需要再调用read/write等系统调用。
下面分析一下mmap的实现原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
SYSCALL_DEFINE6(mmap, unsigned long, addr, unsigned long, len,  
unsigned long, prot, unsigned long, flags,
unsigned long, fd, unsigned long, off)
{
long error;
error = -EINVAL;
if (off & ~PAGE_MASK)
goto out;

error = sys_mmap_pgoff(addr, len, prot, flags, fd, off >> PAGE_SHIFT);
out:
return error;
}

内部直接调用的是sys_mmap_pgoff函数,流程如下:

再经过vm_mmap_pgoff到do_mmap_pgoff,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
unsigned long do_mmap_pgoff(struct file *file,  
unsigned long addr,
unsigned long len,
unsigned long prot,
unsigned long flags,
unsigned long pgoff,
unsigned long *populate)
{
// 申请一个vm_area_struct结构体
struct vm_area_struct *vma;

// ...

// 为vma分配内存
vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
if (!vma)
goto error_getting_vma;

// ... 初始化相关

// 如果是文件映射,给文件添加一个引用计数
if (file) {
region->vm_file = get_file(file);
vma->vm_file = get_file(file);
}

down_write(&nommu_region_sem);

// ...

// 真正去做文件映射
if (file && vma->vm_flags & VM_SHARED)
ret = do_mmap_shared_file(vma);
else
ret = do_mmap_private(vma, region, len, capabilities);

// ...
// 将vma插入到链表中
add_vma_to_mm(current->mm, vma);
// ...
}

在做文件映射时,如果不是共享的文件,则调用的是do_mmap_private函数,此函数流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static int do_mmap_private(struct vm_area_struct *vma,  
struct vm_region *region,
unsigned long len,
unsigned long capabilities)
{
// ...
if (capabilities & NOMMU_MAP_DIRECT) {
// 调用文件映射的方法
ret = vma->vm_file->f_op->mmap(vma->vm_file, vma);
// ...
}

// ...
}

此处f_op->mmap指向的是generic_file_mmap:

1
2
3
4
5
6
7
8
9
10
int generic_file_mmap(struct file * file, struct vm_area_struct * vma)  
{
struct address_space *mapping = file->f_mapping;

if (!mapping->a_ops->readpage)
return -ENOEXEC;
file_accessed(file);
vma->vm_ops = &generic_file_vm_ops;
return 0;
}

内部就是给前面提到的vm_ops函数指针的集合赋值,generic_file_vm_ops指向的是针对文件操作的一系列函数:

1
2
3
4
5
const struct vm_operations_struct generic_file_vm_ops = {  
.fault = filemap_fault,
.map_pages = filemap_map_pages,
.page_mkwrite = filemap_page_mkwrite,
};

其中包括缺页处理,映射页,置为可写三个操作;其中缺页异常的处理逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int filemap_fault(struct vm_area_struct *vma, struct vm_fault *vmf)  
{
// ...

// 先判断当前页有没有被cache
page = find_get_page(mapping, offset);
if (likely(page) && !(vmf->flags & FAULT_FLAG_TRIED)) {
// 预读机制,从cache中拿到数据
do_async_mmap_readahead(vma, ra, file, page, offset);
} else if (!page) {
// 未cache到,直接同步读取
do_sync_mmap_readahead(vma, ra, file, offset);
count_vm_event(PGMAJFAULT);
mem_cgroup_count_vm_event(vma->vm_mm, PGMAJFAULT);
ret = VM_FAULT_MAJOR;
retry_find:
page = find_get_page(mapping, offset);
if (!page)
goto no_cached_page;
}

// ...

// 找到对应页将其赋值给vmf,并返回
vmf->page = page;
return ret | VM_FAULT_LOCKED;
// ...
}

总结mmap文件映射过程:
a) 用户在进程中触发mmap操作;
b) 内核对参数做基本的校验,并针对映射长度做一些内存对齐;
c) 分配vm_area_struct结构,并对其进行初始化;
d) 调用文件系统的mmap映射,将缺页异常等函数指针赋于vm_ops;
e) 将新建的vm_area_struct结构插入到mm链表中;
f) 当进程访问这片内存时,引发缺页异常,从而调用filemap_fault;
g) 缺页异常查找cache中有无请求的页,如果没有,内核发起请求将数据从磁盘装入内存;

3、mmap和read/write的区别

read的系统调用的流程大概如下图所示:

a) 用户进程发起read操作;
b) 内核会做一些基本的page cache判断,从磁盘中读取数据到kernel buffer中;
c) 然后内核将buffer的数据再拷贝至用户态的user buffer;
d) 唤醒用户进程继续执行;

而mmap的流程如下图所示:

内核直接将内存暴露给用户态,用户态对内存的修改也直接反映到内核态,少了一次的内核态至用户态的内存拷贝,速度上会有一定的提升;

4、总结

经过对linux内核源码的一些剖析,对mmap的原理会有更深层次的理解 。 mmap的优点有很多,相比传统的read/write等I/O方式,直接将虚拟地址的区域映射到文件,没有任何数据拷贝的操作,当发现有缺页时,通过映射关系将磁盘的数据加载到内存,用户态程序直接可见,提高了文件读取的效率。对索引数据这种大文件的读取、cache、换页等操作直接交由操作系统去调度,间接减少了用户程序的复杂度,并提高了运行效率。

今天正好在群里跟朋友聊volatile的问题,测了一下gcc对volatile变量和正常变量的实现区别,顺便记一下。

备注:此volatile是在c语言在gcc下的测试结果和原理,和java里的volatile完全两个概念,不要混淆!不要混淆!不要混淆!

首先写一个很简单的demo:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int a = 100;

void foo() {
while (a > 10) {}
}

int main() {
foo();
return 0;
}

重点就是看反编译后的foo函数的实现,循环判断a是否大于10,然后看a是普通变量和volatile修饰变量的差异:

普通变量的gcc编译后的汇编代码如下:

1
2
3
4
5
6
7
8
0000000000000620 <foo>:
620: 8b 05 ea 09 20 00 mov eax,DWORD PTR [rip+0x2009ea] # 将a的值从内存加载至寄存器
626: 66 2e 0f 1f 84 00 00 nop WORD PTR cs:[rax+rax*1+0x0]
62d: 00 00 00
630: 83 f8 0a cmp eax,0xa # 将eax寄存器的值与0xa(10)做对比
633: 7f fb jg 630 <foo+0x10> # jg,如果大于目标值,跳转至630,继续对比eax和0xa
635: f3 c3 repz ret
637: 66 0f 1f 84 00 00 00 nop WORD PTR [rax+rax*1+0x0]

对变量a加上volatile修饰后的汇编代码如下:

1
2
3
4
5
6
0000000000000610 <foo>:
610: 8b 05 fa 09 20 00 mov eax,DWORD PTR [rip+0x2009fa] # 将a的值从内存加载至寄存器
616: 83 f8 0a cmp eax,0xa # 将eax寄存器的值与0xa(10)做对比
619: 7f f5 jg 610 <foo> # jg,如果大于目标值,跳转至610,重新将a的值从内存加载至eax
61b: f3 c3 repz ret
61d: 0f 1f 00 nop DWORD PTR [rax]

可以明显的看到,未加volatile的汇编是每次只跟eax寄存器中的值做对比,而加上volatile之后,是跳转至610,也就是会重新将a的值从内存加载至寄存器,保证每次a的值都是最新,从而保证了“可见性”

前段时间为了做一个分享,实现了一个简单的搜索引擎(https://github.com/imaben/ts-engine), 其中有一个标记删除的地方用到了bitmap,刚开始没有想那么复杂,真正写起来,发现需要注意的一些细节还是不少的,所以在此记录一下开发过程中的一些心得,主要是一些位运算相关的点。

代码链接:https://github.com/imaben/ts-engine/blob/master/bitmap.h

首先说一下bitmap的原理,bitmap的应用非常广泛,它通常是以bit位来标记某个id的状态,优点是占用空间小,查询效率高,1byte = 8bit,假如一共1000万的数据量,总共算下来占用的空间也只有 10000000/8/1024 = 1220KB, 刚刚有1MB多一点。查询时直接根据id来找到对应的byte,然后做位于运算,直接可以拿到结果,由于不涉及到复杂运算,寻址的范围也很小,所以整体的查询效率非常高。

但这种方式本身也有局限性,就是要标记的id最好是从1开始自增的情况,如果id完全没有规律,则很容易出现内存空洞的情况。

bitmap结构体的定义如下:

1
2
3
4
typedef struct {
uint8_t *c;
uint64_t l;
} ts_bitmap_t;

结构体本身比较简单,一个uint8_t(char)类型的指针,指向一块保存bitmap数据的内存,一个uint64_t类型的长度,用来标记c的长度。有一些做法是采用int型指针来保存数据,但在跨平台时,可能要考虑大小端的问题,但个人感觉没有本质上的区别。

第一步要做的就是要保证每次set时c的长度是足够的,所以需要在每次set时,都需要检查一下c的长度,以防止内存访问越界的情况发生。但是为了减少内存频繁分配的开销,一般都会有一些预分配的做法,比如按照128字节做对齐,当小于128字节时,也分配128字节的大小,当是129字节时,就分配256字节,以此类推。这样做有两点好处,一是避免递增访问时,内存频繁分配,二是保证整个buffer的内存大小总是cpu cacheline的整倍数,从而提高cpu缓存的存取性能。

首先要实现上面提到的预分配逻辑:假如按照比较传统的写法应该是这样(伪代码):

1
2
3
4
5
6
7
8
9
10
#define PREALLOC 128
int mem_align(int size) {
if (size <= PREALLOC) {
return PREALLOC;
}
// 求size是PREALLOC的多少倍
// 向上取整并乘以PREALLOC
int multiple = ceil(size / PREALLOC);
return multiple * PREALLOC;
}

但这样显然不太优雅,性能上开销也比较大,如果通过位运算来解决呢?
举两个例子,假如有10和129,我们需要最终转换为128和256,如下表:

10进制 2进制 目标2进制
10 00001010 10000000
129 10000001 100000000

10来举例,既然要做128字节对齐,我们不妨先把它加个128:

1
10 + 128 = 138 = 10001010

跟我们的目标值已经很像了,我们只需要把最高位1后面的所有的非0,全部置为0就达到目的了,可以使用反码加位与的操作解决,如下:

1
2
3
4
5
128 - 1 = 01111111 // 先将需要置为0的位数全部置为1

~(128 - 1) = 11111111 11111111 11111111 10000000 // 按位取反,以32位整数为例

(10 + 128) & (~128 - 1) = 10000000 // 成功转换

但是这个地方有一个极端情况,就是当值正好是128时,用上面的算法算出来就是256了,所以再处理一下就是

1
(10 + 128 - 1) & (~128 - 1)

这样就解决了上面所说的问题。

最终预分配的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifndef TS_BITMAP_PREALLOC
#define TS_BITMAP_PREALLOC 128LL
#endif

#define ts_bmp_align(s) ((s + TS_BITMAP_PREALLOC - 1) & ~(TS_BITMAP_PREALLOC - 1))

#define ts_bitmap_reserve(bm, n) { \
if ((n) > ((bm)->l)) { \
uint64_t __len = ts_bmp_align(n); \
(bm)->c = ts_realloc((bm)->c, __len); \
assert((bm)->c != NULL); \
bzero((void *)((bm)->c + (bm)->l), __len - (bm)->l); \
(bm)->l = __len; \
} \
}

小细节:

在上面按位取反的时候,需要注意32位与64位的问题,假如有以下函数:

1
2
3
4
uint64_t mem_align(uint64_t i, int align)
{
return (i + align - 1) & ~(align - 1);
}

看似很和谐,其实隐藏着一个大坑,我们将这段代码编译成汇编(GCC),生成如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
_mem_align:                             ## @mem_align
.cfi_startproc
## %bb.0:
push rbp
.cfi_def_cfa_offset 16
.cfi_offset rbp, -16
mov rbp, rsp
.cfi_def_cfa_register rbp
mov qword ptr [rbp - 8], rdi
mov dword ptr [rbp - 12], esi
mov rdi, qword ptr [rbp - 8]
movsxd rax, dword ptr [rbp - 12]
add rdi, rax
sub rdi, 1
mov esi, dword ptr [rbp - 12]
sub esi, 1
xor esi, -1
movsxd rax, esi
and rdi, rax
mov rax, rdi
pop rbp
ret
.cfi_endproc
## -- End function
.globl _main ## -- Begin function main
.p2align 4, 0x90

可以看到在15行至17行在对align做-1并取反的操作中,竟然是在32位寄存器中进行的,这样就会导致当i的值超过32位时,取反后的结果总是等于0,进而得到的结果也会是0,解决办法很简单,就是把align的类型也改为64位即可。
这种情况不同编译器可能会有不同的行为,所以为了稳妥起见,故在定义128长度的时候,改成了64位的128LL。

—–分割线—–

解决完内存的问题,再看一下怎么根据id定位到id应该存储的byte上,因为1byte = 8bit,所以也就是单纯的除以8即可,用位运算操作数右移三位即可:

1
#define ts_bmp_dig(p) ((p) >> 0x3)

找到了对应的字节,再要在字节中找到每N位,直接对8取模即可:

首先8的二进制为1000,模除只需要将二进制后三位置为1,再与操作数做位与操作:

1
#define ts_bmp_mod(p) ((p) & 0x7)

需要注意的是这种方式只有在模除数是2的n次方时有效。

—–分割线—–

下面就是操作set、unset和get了,set最简单:

1
2
3
4
#define ts_bitmap_set(bm, p) { \
ts_bitmap_reserve(ts_bitmap_p(bm), ts_bmp_dig(p) + 1); \
ts_bitmap_p(bm)->c[ts_bmp_dig(p)] |= (1 << ts_bmp_mod(p)); \
}

直接通过dig找到对应byte后,将1左移取模后的第n位,做位或操作即可。

unset操作如下:

1
2
3
#define ts_bitmap_unset(bm, p) { \
if (ts_bmp_dig(p) < (bm)->l) ts_bitmap_p(bm)->c[ts_bmp_dig(p)] &= ~(1 << (ts_bmp_mod(p))); \
}

先判断如果unset操作数的所在的字节已经超过buffer的总长度,直接忽略。
然后为了在unset时不影响当前byte的其它位,故需要左移后做个取反,再与原数做位于操作。

get操作如下:

1
2
#define ts_bitmap_get(bm, p) ((ts_bmp_dig(p) >= ((ts_bitmap_p(bm))->l) ? 0 : \
((ts_bitmap_p(bm)->c[ts_bmp_dig(p)]) >> ts_bmp_mod(p))) & 1)

和unset一样做一个长度的基本判断,然后按照对应byte,将对应的bit右移到最后一位,与1做位于操作。

至此,bitmap的实现完成。

时隔多年,博客又重新开张了!