linux指针赋值原子,x86_64处理器的指针赋值是原子操作吗?

如题, x86_64处理器的指针赋值是原子操作吗?

说实话我很讨厌参与讨论那些似乎不确定东西,倒不是说我对未知不敬畏,而是参与讨论的人大多数都是似懂非懂,对,我说的不确定性指的是参与讨论的人的认知的不确定,如果你自己都似懂非懂,那么我说什么你都可以反驳我,说些 “貌似,可能,并不绝对” 的词汇来让事情变得混乱。

所以说,我的意思是, 以Intel的手册为准。 我觉得我应该收回上面的那句 “which is a pointer assignment operation, on the x86_64 platform which is an atomic operation.” 然后重新说出个所以然来。

有了这段 权威描述 ,本文似乎该结束了。

但是…

show me the code 是个金句,它鄙视的是 talk, 因为 talk is cheap.

那好吧,按照这样看来,上面那一段都是废话,非常cheap,那么下面就用代码说话吧。

声明一下:

以下无术语,也不再引用任何cheap的东西。

本文虽然以“指针赋值”为例,但其实任何“int,long,long long”赋值均试用 。

我在代码里强转来强转去的,实际在暗示什么呢?CPU和内存不了解什么是指针,只有在执行的时候,一个unsigned long才会 变成 指针。

网络协议,序列化时,请 谨慎相信任何原子操作的承诺!

考虑一个指针p,两个或者多个线程分别对它进行赋值:

long *p;

// thread 1

p = a1;

// thread 2

p = a2

结果可以预期吗?如果你笃信指针赋值是原子操作,那么最终结果,p不是a1,便是a2,这是确定的。

然而Intel手册里说,如果指针p跨越了cacheling的边界,便不能保证赋值操作是原子的,为了复现Intel的说法,从而证明指针赋值并非原子的,只需要给出一个反例,即p既不是a1,也不是a2。

在编码之前,我们先查一下自己实验的机器上的cacheline的大小:

root@zhaoya-VirtualBox:~/xdp/msvc# cat root@zhaoya-VirtualBox:~# cat /sys/devices/system/cpu/cpu1/cache/index0/coherency_line_size

64

OK,64字节的cacheline,我们下面就制造一个跨越64字节边界的指针,直接看代码:

#include

#include

// 不让编译器自动扩充结构体对齐特征,这个在网络协议以及序列号操作中很常见。

#pragma pack(1)

struct pack {

// 60字节的padding

char unsued[60];

// cacheline仅剩下4字节,仅够装指针p的4字节,剩余4字节跨越到另一个cacheline

long *p;

};

#pragma pack()

// 内存对齐粒度最小为1字节,我设置64个元素的数组,总有一个元素恰好是在64字节边界的!

struct pack pa[64];

// 保存64字节边界的元素

struct pack *used;

long a = 0x1122334455667788;

long b = 0x8877665544332211;

void *write_value1(void *param)

{

for (;;) {

used->p = (long *)a;

// 不让编译器优化

asm volatile("" ::: "memory");

}

return NULL;

}

void *write_value2(void *param)

{

for (;;) {

used->p =(long *)b;

asm volatile("" ::: "memory");

}

return NULL;

}

int main(int argc, char **argv)

{

int i;

long *p;

pthread_t t1, t2;

// for循环找到那个64字节边界的pack结构体元素

for (i = 0; i < 64; i++) {

unsigned long addr;

addr = (unsigned long)&pa[i];

if (addr%64 == 0) {

// 赋值给used

used = (struct pack *)addr;

break;

}

}

pthread_create(&t1, NULL, write_value1, NULL);

pthread_create(&t2, NULL, write_value2, NULL);

while (1) {

p = used->p;

// 我们看能不能找到既不是a,又不是b的p

if (p != (long *)a && p != (long *)b) {

printf("%lx\n", (unsigned long)p);

}

}

return 0;

}

跑一下呗:

8899615b483992928ab4ed4b638f24e9.png

我的天!

这意味着什么?

这意味着不加锁的指针赋值,极其危险!

什么?难道编译器不帮忙?

编译器只能尽量帮忙,哦,对了,还有Linux内核的伙伴系统,slab系统,都只是尽量帮忙,其余的事,只能看造化。谁也不能保证你不会写出上面类似pack的结构体,因此结构体的字段布局非常重要。

即便这样,你也不能保证结构体对象恰好被载入你希望的位置,只要超过一个cacheline大小的结构体,内部字段的赋值就一定小心再小心:

加锁影响性能

不加锁怕跨越边界

说白了这就是个手艺活。

如果你想快速复现指针跨越cacheline的赋值非原子性,直接加个修饰即可:

// 你也可以直接用aligned来固化地址对齐特征,然则不真实!

struct pack pa __attribute__ ((aligned(64)));

...

int main(...

...

used = &pa;

不多说了,深入什么单条指令的原子执行,LOCK引脚,cache一致性原子操作粒度等等细节无益于解决实际问题,至于其它体系结构,摸都摸不到(我是说我自己),更显得纸上谈兵,到此为止了。

浙江温州皮鞋湿,下雨进水不会胖!

f5fdd7608baa8cefd6a32c4be7c2639e.png

2f629b3873c4a35a79c201f33b3e2cf3.png

dog250

博客专家

发布了1546 篇原创文章 · 获赞 4762 · 访问量 1057万+

他的留言板

关注

标签:x86,long,64,赋值,字节,指针,pack

来源: https://blog.csdn.net/dog250/article/details/103948307

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值