250629---对负数取模引发的线程死锁问题
前言
项目背景
给客户做项目,做到利用信号量控制线程同步互斥的时候
做一个经典的 Cigarette-Smoker 问题 (https://pages.mtu.edu/~shene/NSF-3/e-Book/SEMA/TM-example-smoker.html)
问题描述
假设一个系统有三个抽烟者进程和一个供应者进程。每个抽烟者不停地卷烟 并抽掉它,但是要卷起并抽掉一支烟,抽烟者需要有三种材料:烟草、纸和胶水。三个抽烟 者中,第一个拥有烟草、第二个拥有纸,第三个拥有胶水。供应者进程无限地提供三种材料, 供应者每次将两种材料放到桌子上,拥有剩下那种材料的抽烟者卷一根烟并抽掉它,并给供 应者一个信号告诉完成了,供应者就会放另外两种材料在桌上,这种过程一直重复(让三个 抽烟者轮流地抽烟)。
问题现象描述
出现了死锁情况:
如下图所示,agent 会一直等待smoker结束吸烟,但是不知道为啥吸烟者也一直等待agent提供材料。
并且我们可以看到,agent最后一次 waiting for smoker finish
打印,并没有对应的 generate xx and xx
我当时也没多想,也急着交付实验报告给客户。
初步自行排查
代码逻辑排查
我写的代码逻辑基本和殖呼上面的一样
https://zhuanlan.zhihu.com/p/442196049
AI分析
问AI,AI分析告诉我说
问hackergame群友
我最后请教了hackergame的群友,他们都挺厉害的。
最后一个群友给我一针见血的指出问题了:
因为 4294967296 这个数,是 0x1 0000 0000, 其实已经超过正数 int 的表示范围了,是负数了。
下面的代码里,ingred=rand%3
而由于rand是负数,而负数对3取模的结果仍然是一个负数
int ingred = rand % 3;
// 使用线性同余法更新 rand
rand = (1664525 * rand + 1013904223) % 4294967296;
就导致后面的switch没办法进入正常的分支里,去创建正常的资源。
switch (ingred) {
case EXCEPT_TABACCO:
sem_post(&except_tabacco);
printf("agent: generate paper and match\n");
break;
case EXCEPT_PAPER:
sem_post(&except_paper);
printf("agent: generate tabacco and match\n");
break;
case EXCEPT_MATCH:
sem_post(&except_match);
printf("agent: generate paper and tabacco\n");
break;
default:
break;
}
负数取模情况的讨论
来自群友
准确来说有这样一个关系式:
(a/b)b + a%b == a,“/”是整除,向0取整。
当a、b都是正整数的时候最简单,“/”就是向下(0)取整的除法,a/b、a%b就是商和余数。
当a是负整数、b是正整数的时候“/”向上(0)取整,这个时候(a/b)b比a大,a%b就是负数,
然后由于此时向上取整的a/b可以看作向下取整的-(-a/b),所以a%b就相当于(-a/b)*b-(-a),也就是-(-a)%b。
b是负整数的时候可以类似讨论。
勘误和补充
这里说的有问题
因为 4294967296 这个数,是 0x1 0000 0000, 其实已经超过正数 int 的表示范围了,是负数了。
群友的补充
不过其实那个0x100000000(也就是2^32)对应于int类型的0,所以逻辑上来说是一个除零操作,但是编译器在编译的时候是把它当作几个移位操作来处理的所以没报错
如图所示,编译器会把除以0x100000000 转成移位操作,相当于什么都没干
所以这就相当于啥也没干,然后你的线性同余生成器(LCG)在迭代rand的时候,可能会出现最高位是1的数字(也就是在231~232-1之间)
这个时候因为补码表示它已经是负数了,根据刚刚提到的模运算操作,当这个数不能被3整除时,ingred = rand%3 得到的是一个负数(当能被3整除时ingred得0)
补充-取余运算的汇编实现
因为字面值2^32已经超过了int的表示范围了,所以编译器把它当64位的数看,故使用64位的寄存器来运算,这是其一。
其二是,对于模数是28、216、2^32的情况(也就是int8、int16、int32的尺寸),编译器会采用这种与寄存器尺寸对齐的方式运算,比如下面的-114模256的例子
它的效果就是清掉eax的高32-8=24比特,然后再根据被除数的符号位决定要不要恢复符号位。
对于其他的2的指数的模数而言,用的是and和一个掩码,比如下面是-114模4096
除此以外的情况就是用的imul,也就是一般的余数计算方法了,下面是-114模0x114514
Italian Cook 22:09:27
还真是细节啊,连模数的尺寸和是否为2的指数这样的情况都会生成不一样的汇编代码
我是第一次了解到这个知识,大开眼界了
群友
是这样的,因为8位、16位、32位都是和al、ax、eax对齐的,用它们更快, 而且指令更短
Italian Cook 22:41:39
我好像能看懂一点了,cdq 是符号扩展,先把符号位全部赋予了edx
然后 add eax , edx 制造一次溢出,再进行 movzx 零扩展,把在高8位的符号位都清零
然后再 sub eax,edx 恢复一下符号位
Italian Cook 22:42:38
因为8、16、32位能对齐到寄存器al,ax,eax,所以可以用 movzx
不然的话,普通的2的指数的模数,就用and来清零符号位
群友
嗯对,差不多就是这样的
修改记录及gitee
https://gitee.com/wangqiyuejava63/os-labs/commit/0b707260e119f1727c3e42a9fe417276fff4f9c6