前言
本文将有侧重的介绍CSAPP
第二章中无符号数有符号数表示及其相互转换部分,并通过其在Linux
内核队列kfifo
中的应用来帮大家更好意识到理解计算机数值系统的现实作用。让大家理解到他们不只是躺在书上的公式那么简单。
文中对于大家普遍不太爱看的数学公式,我尽量解释清楚,希望读者耐心看完。
数值系统
由于指令和数据被统一存储在内存中不做区分,同样一组二进制序列它可以被解读为无符号数,有符号数,字符和指令。
指令和数据放在内存中,CPU区分它们的依据为指令周期的不同阶段。对于CPU而言,取指周期取出的是指令;分析、取数或执行周期取出的是数据。指令地址来源于程序计数器;数据地址来源于地址形成部件。
无符号数编码
对于一组二进制序列向量
x
→
=
[
x
w
−
1
,
x
w
−
2
,
.
.
.
.
.
.
,
x
0
]
\overrightarrow{x}=[x_{w-1},x_{w-2},......,x_0]
x=[xw−1,xw−2,......,x0],无符号数编码方式为:
B
2
U
w
(
x
→
)
=
∑
i
=
0
w
−
1
x
i
2
i
B2U_w(\overrightarrow{x})=\displaystyle\sum_{i=0}^{w-1}x_i2^i
B2Uw(x)=i=0∑w−1xi2i ,其中,B
表示Binary
,U
表示Unsigned
对于无符号数编码方式,
w
w
w个bit
表示下,可表示的范围为
[
0
,
2
w
−
1
]
[0, 2^w-1]
[0,2w−1]。
最小值为每一个bit
全0则为0,最大值为每一个bit
都为1,则为
2
w
−
1
2^w-1
2w−1
例如,对于
1011
1011
1011这个二进制序列,我们可以按照公式计算其值为
1
∗
2
0
+
1
∗
2
1
+
0
∗
2
2
+
1
∗
2
3
=
11
1*2^0+1*2^1+0*2^2+1*2^3=11
1∗20+1∗21+0∗22+1∗23=11
有符号数编码
计算机中采用二进制补码的形式表示有符号数据,其中最高位表示符号位。CSAPP
中对补码的解释形式如下。
对于一组二进制序列向量
x
→
=
[
x
w
−
1
,
x
w
−
2
,
.
.
.
.
.
.
,
x
0
]
\overrightarrow{x}=[x_{w-1},x_{w-2},......,x_0]
x=[xw−1,xw−2,......,x0],有符号数编码方式为:
B
2
T
w
(
x
→
)
=
−
x
w
−
1
2
w
−
1
+
∑
i
=
0
w
−
2
x
i
2
i
B2T_w(\overrightarrow{x})=-x_{w-1}2^{w-1}+\displaystyle\sum_{i=0}^{w-2}x_i2^i
B2Tw(x)=−xw−12w−1+i=0∑w−2xi2i。其中,B
表示Binary
,T
表示Two’s-Complement 补码
对于有符号数编码方式,
w
w
w个bit
表示下,可表示的范围为
[
−
2
w
−
1
,
2
w
−
1
−
1
]
[-2^{w-1}, 2^{w-1}-1]
[−2w−1,2w−1−1]
按照编码方式的数学公式来解读,最小值为最高位为1,其余为0,此时表示的数为
−
2
w
−
1
-2^{w-1}
−2w−1
最大值为最高位为0,其余位全1,此时表示的数为
2
w
−
1
−
1
2^{w-1}-1
2w−1−1
同样的,对于
1011
1011
1011这个二进制序列,我们可以按照公式计算其值为
−
1
∗
2
3
+
1
∗
2
0
+
1
∗
2
1
+
0
∗
2
2
=
−
5
-1*2^3+1*2^0+1*2^1+0*2^2=-5
−1∗23+1∗20+1∗21+0∗22=−5
有符号数和无符号数表示法的关系
当
x
>
=
0
x>=0
x>=0 ,
x
→
=
[
x
w
−
1
,
x
w
−
2
,
.
.
.
.
.
.
,
x
0
]
\overrightarrow{x}=[x_{w-1},x_{w-2},......,x_0]
x=[xw−1,xw−2,......,x0]最高位为0时,也就是在有符号表示下该数为正数时。无符号数表示法和有符号数表示法对同一个二进制序列得到的结果相同。
例如:二进制序列
0111
0111
0111在两种表示法中都为
7
7
7
当
x
<
0
x<0
x<0,
x
→
=
[
x
w
−
1
,
x
w
−
2
,
.
.
.
.
.
.
,
x
0
]
\overrightarrow{x}=[x_{w-1},x_{w-2},......,x_0]
x=[xw−1,xw−2,......,x0]最高位为1时,也就是在有符号表示下该数为负数时,无符号数表示法和有符号数表示法对同一个二进制序列的除最高位所代表的数值不同外,其余位全部相同。
举例来说:二进制序列
1011
1011
1011有符号数表示法中最高位1位权值为
−
8
-8
−8,无符号数表示法中最高位权值为
8
8
8。剩余位置中所有数字权值相同。有符号下为
−
8
+
2
+
1
-8+2+1
−8+2+1,无符号下为
8
+
2
+
1
8+2+1
8+2+1。
当最高位必然位1时,分解来看公式有:
B
2
U
w
(
x
→
)
=
2
w
−
1
+
∑
i
=
0
w
−
2
x
i
2
i
B2U_w(\overrightarrow{x})=2^{w-1}+\displaystyle\sum_{i=0}^{w-2}x_i2^i
B2Uw(x)=2w−1+i=0∑w−2xi2i
B
2
T
w
(
x
→
)
=
−
2
w
−
1
+
∑
i
=
0
w
−
2
x
i
2
i
B2T_w(\overrightarrow{x})=-2^{w-1}+\displaystyle\sum_{i=0}^{w-2}x_i2^i
B2Tw(x)=−2w−1+i=0∑w−2xi2i
上下两式做个减法,可得
B
2
U
w
(
x
→
)
−
B
2
T
w
(
x
→
)
=
2
w
B2U_w(\overrightarrow{x})-B2T_w(\overrightarrow{x})=2^w
B2Uw(x)−B2Tw(x)=2w
也就是说,对于一个有符号数下小于0的数,取其无符号数表示减去有符号数下的值,结果为 2 w 2^w 2w。同样拿上方提出的二进制序列 1011 1011 1011为例, 11 − ( − 5 ) = 16 = 2 4 11-(-5)=16=2^4 11−(−5)=16=24
到此,我们可以给出4bit表示下对应的无符号数和有符号数的表格帮助大家理解:
∣ B U T 0000 0 0 0001 1 1 0010 2 2 0011 3 3 ∣ \def\arraystretch{1.5} \begin{vmatrix} B & U & T \\ \hline 0000 & 0 & 0 \\ 0001 & 1 & 1 \\ 0010 & 2 & 2 \\ 0011 & 3 & 3 \\ \hdashline \end{vmatrix} B0000000100100011U0123T0123 ∣ B U T 0100 4 4 0101 5 5 0110 6 6 0111 7 7 ∣ \def\arraystretch{1.5} \begin{vmatrix} B & U & T \\ \hline 0100 & 4 & 4 \\ 0101 & 5 & 5 \\ 0110 & 6 & 6 \\ 0111 & 7 & 7 \\ \hdashline \end{vmatrix} B0100010101100111U4567T4567 ∣ B U T 1000 8 − 8 1001 9 − 7 1010 10 − 6 1011 11 − 5 ∣ \def\arraystretch{1.5} \begin{vmatrix} B & U & T \\ \hline 1000 & 8 & -8 \\ 1001 & 9 & -7 \\ 1010 & 10 & -6 \\ 1011 & 11 & -5 \\ \hdashline \end{vmatrix} B1000100110101011U891011T−8−7−6−5 ∣ B U T 1100 12 − 4 1101 13 − 3 1110 14 − 2 1111 15 − 1 ∣ \def\arraystretch{1.5} \begin{vmatrix} B & U & T \\ \hline 1100 & 12 & -4 \\ 1101 & 13 & -3 \\ 1110 & 14& -2 \\ 1111 & 15 & -1 \\ \hdashline \end{vmatrix} B1100110111101111U12131415T−4−3−2−1
综上所述,根据上面两种情况的分别推理,我们有:
B
2
U
w
(
x
→
)
=
{
B
2
T
w
(
x
→
)
+
2
w
if
x
<
0
B
2
T
w
(
x
→
)
if
x
>
=
0
B2U_w(\overrightarrow{x})= \begin{cases} B2T_w(\overrightarrow{x})+2^w &\text{if } x<0 \\ B2T_w(\overrightarrow{x}) &\text{if } x>=0 \end{cases}
B2Uw(x)={B2Tw(x)+2wB2Tw(x)if x<0if x>=0 其中,
x
x
x大于小于0的关键在于最高位。
所以我们可以化简上式如下: B 2 U w ( x → ) = B 2 T w ( x → ) + x w − 1 2 w B2U_w(\overrightarrow{x})=B2T_w(\overrightarrow{x})+x_{w-1}2^w B2Uw(x)=B2Tw(x)+xw−12w
无符号数加法
对于
w
w
wbit表示的两个无符号数相加,结果如下:
a
+
b
=
{
a
+
b
if
n
o
r
m
a
l
a
+
b
−
2
w
if
o
v
e
r
f
l
o
w
a+b = \begin{cases} a+b &\text{if } normal \\ a+b-2^w &\text{if } overflow \end{cases}
a+b={a+ba+b−2wif normalif overflow
溢出后,结果无法用
w
w
wbit表示,此时只取最后
w
w
wbit。
对于两个无符号数
x
,
y
(
0
≤
x
,
y
≤
U
M
a
x
w
)
x, y(0\le x,y \le UMax_w)
x,y(0≤x,y≤UMaxw),取
s
=
x
+
y
s=x+y
s=x+y,那么当且仅当
s
<
x
s<x
s<x时发生加法溢出(
s
<
y
s<y
s<y同理)。
注:对于
w
w
wbit表示的无符号数,
U
M
a
x
w
=
2
w
−
1
UMax_w=2^w-1
UMaxw=2w−1
证明如下:当发生溢出时,加和
s
=
x
+
y
−
2
w
s=x+y-2^w
s=x+y−2w,由条件可知,
0
≤
x
≤
U
M
a
x
w
<
2
w
,
0
≤
y
≤
U
M
a
x
w
<
2
w
0 \le x \le UMax_w < 2^w, 0\le y \le UMax_w <2^w
0≤x≤UMaxw<2w,0≤y≤UMaxw<2w。所以
x
−
2
w
<
0
x-2^w<0
x−2w<0 且
y
−
2
w
<
0
y-2^w<0
y−2w<0,带回原式有
s
=
x
+
(
y
−
2
w
)
<
x
s=x+(y-2^w)<x
s=x+(y−2w)<x 且有
s
=
y
+
(
x
−
2
w
)
<
y
s=y+(x-2^w)<y
s=y+(x−2w)<y
无符号数减法
对于 w w wbit表示的两个无符号数相减,结果如下: a − b = { a − b if a ≥ b a − b + 2 w if a < b a-b = \begin{cases} a-b &\text{if } a \ge b\\ a-b+2^w &\text{if } a<b \end{cases} a−b={a−ba−b+2wif a≥bif a<b
减法结果小于零时,又回到上文中阐述的第二种情况所示
B
2
U
w
(
x
→
)
=
B
2
T
w
(
x
→
)
+
2
w
B2U_w(\overrightarrow{x})=B2T_w(\overrightarrow{x})+2^w
B2Uw(x)=B2Tw(x)+2w。
由于
a
−
b
<
0
a-b<0
a−b<0则其对应的二进制序列下无符号数表示的值就需要做对应调整,请读者揣摩。
Linux内核队列kfifo
下面对kfifo
的介绍中将着重介绍和数值系统密切相关的部分,而不去介绍其无锁队列等特性,其中列出的函数也是简化版本,部分函数并非内核中的原始代码。
给出一个内核版本为2.6.34
中kfifo
的关键结构体定义:
struct kfifo {
unsigned char *buffer; /* 存储数据的缓冲区 */
unsigned int size; /* 缓冲区大小 */
unsigned int in; /* 入队位置 */
unsigned int out; /* 出队位置 */
};
kfifo
内核队列为了通用性,不只局限于某一种特定数据类型,因此在队列的设计中使用in out
两个指针作为下标指向unsigned char
类型的缓冲区,每次入队和出队时按照要操作的数据的字节数操作队列。
假设我们当前在32位系统下同时我们有数据类型struct Student
内容如下:
struct Student{
char *name;
};
那么,需要入队时,则以in % size
位下标,所其指向的缓冲区buffer
位置开始,占据缓冲区中的4
个字节,也就是32bit
存储该数据。同理,需要出队时,从out % size
开始,从buffer
取出4字节作为出队的Student
的内容。
队列缓冲区长度限制
内核队列限制条件:队列缓冲区的大小必须为2的整数次幂,且不为1。 这里的理由非常简单,由于我们会频繁用到取余机制获取出队入队的下标,所以内核把取余操作变为与运算,能够大大加快运算速度。而运算成立的条件就是队列缓冲区大小必须为2的整数次幂。
kfifo->in & (kfifo->size -1)
以及kfifo->out & (kfifo->size -1)
假设缓冲区大小为
2
n
2^n
2n,那么
s
i
z
e
−
1
size-1
size−1 对应得二进制序列每一位都为1,即
[
x
n
−
1
=
1
,
x
n
−
2
=
1
,
.
.
.
.
.
.
x
0
=
1
]
[x_{n-1}=1,x_{n-2}=1,......x_0=1]
[xn−1=1,xn−2=1,......x0=1]
那么利用位运算中的与运算可以达到取余的效果,且运算速度大大加快。
队列操作辅助函数
重置队列函数
static inline void kfifo_reset(struct kfifo *fifo)
{
fifo->in = fifo->out = 0;
}
返回队列已用空间大小
static inline unsigned int kfifo_len(struct kfifo *fifo)
{
return fifo->in - fifo->out;
}
这里是需要引起注意的地方,由于in out
两个指针只增不减,考虑溢出发生时还能保证结果仍旧是队列已用空间大小的正确性原理。下方会详细解释。
队列判空函数
static inline __must_check int kfifo_is_empty(struct kfifo *fifo)
{
return fifo->in == fifo->out;
}
队列判满函数
static inline __must_check int kfifo_is_full(struct kfifo *fifo)
{
return kfifo_len(fifo) == kfifo_size(fifo);
}
返回队列可用空间大小
static inline __must_check unsigned int kfifo_avail(struct kfifo *fifo)
{
return kfifo_size(fifo) - kfifo_len(fifo);
}
入队操作
unsigned int kfifo_in(struct kfifo *fifo, const void *from,
unsigned int len)
{
len = min(kfifo_avail(fifo), len);
unsigned int off = fifo->in % fifo->size;
/* first put the data starting from fifo->in to buffer end */
l = min(len, fifo->size - off);
memcpy(fifo->buffer + off, from, l);
/* then put the rest (if any) at the beginning of the buffer */
memcpy(fifo->buffer, from + l, len - l);
fifo->in += len;
return len;
}
函数难懂的点在于:当入队操作涉及到缓冲区的边界且从in
指针到边界的距离不满足待入队空间时,此时需要从缓冲区头部放入数据。
如下图所示:蓝色表示新的入队数据
出队操作
unsigned int kfifo_out(struct kfifo *fifo, void *to, unsigned int len)
{
len = min(kfifo_len(fifo), len);
unsigned int off = fifo->out % fifo->size;
/* first get the data from fifo->out until the end of the buffer */
unsigned int l = min(len, fifo->size - off);
memcpy(to, fifo->buffer + off, l);
/* then get the rest (if any) from the beginning of the buffer */
memcpy(to + l, fifo->buffer, len - l);
fifo->out += len;
return len;
}
出队函数难懂的点在于:当出队操作涉及到缓冲区的边界且从out
指针到边界的距离不满足待出队空间时,此时需要从缓冲区头部出队剩余数据。
如下图所示:蓝色表示待出队数据
溢出核心机制
in out
两个unsigned int
类型指针在入队出队操作只增不减,必然会达到上界,进而溢出,从0开始继续递增。
大家考虑问题一:in
指针在入队时会不会在溢出之后再增加超过out
?
这必然是不会的,上文入队操作中我们知道,每次入队的长度为当前可用长度和待入队数据中的最小值,所以由于入队长度的限制,这种情况必然不会发生。
考虑问题二:out
指针在出队时会不会在溢出之后再增加超过in
?
同理,根据出队函数中出队大小为当前队列被占用长度和待出队元素长度的最小值,我们知道,由于出队长度的限制,out
指针即便溢出之后也不会超过in
。
那么,我们再考虑问题三:为什么任何情况下unsigned int len=in - out
都能表示队列已用长度?
普通情况下:in
位于out
之前,那么结果显然正确。
溢出情况下,in < out
,此时in - out < 0
,结果由无符号数接收,则需要对结果二进制序列用无符号数表示法解读。
那么好,根据上文我们的推导:无符号数相减,结果小于0时为:
a
−
b
=
a
−
b
+
2
w
a-b =a-b+2^w
a−b=a−b+2w。而文中,in out
类型都是无符号整数,那么结果变为
i
n
−
o
u
t
=
i
n
−
o
u
t
+
2
32
in - out=in - out + 2^{32}
in−out=in−out+232。建立在无符号整数长度为32bit时。
下面对当
i
n
<
o
u
t
in<out
in<out 时,
i
n
−
o
u
t
+
2
32
=
l
e
n
in-out+2^{32}=len
in−out+232=len 做证明。此时大致布局如下所示:
我们假设缓冲区边缘部分长度为
a
a
a,缓冲区开头部分长度为
b
b
b,那么
a
+
b
=
l
e
n
a+b=len
a+b=len,其中,
0
<
a
,
b
<
s
i
z
e
0<a, b <size
0<a,b<size。
按照上述假设,此时
o
u
t
=
2
32
−
a
out=2^{32}-a
out=232−a,
i
n
=
b
in = b
in=b,那么
i
n
−
o
u
t
=
b
−
2
32
+
a
in - out = b-2^{32}+a
in−out=b−232+a 因为结果小于0,故而真实结果为
i
n
−
o
u
t
=
(
b
−
2
32
+
a
)
+
2
32
=
a
+
b
=
l
e
n
in - out = (b-2^{32}+a )+2^{32}=a+b=len
in−out=(b−232+a)+232=a+b=len。证毕
最后还要请读者思考一个问题
从下面的表格里我们看到,4bit表示下负数最大为-8,此时对应正数8,这是4bit能表示的最小负数,也是有负数相对应下的无符号数的最小值。
∣ B U T 0000 0 0 0001 1 1 0010 2 2 0011 3 3 ∣ \def\arraystretch{1.5} \begin{vmatrix} B & U & T \\ \hline 0000 & 0 & 0 \\ 0001 & 1 & 1 \\ 0010 & 2 & 2 \\ 0011 & 3 & 3 \\ \hdashline \end{vmatrix} B0000000100100011U0123T0123 ∣ B U T 0100 4 4 0101 5 5 0110 6 6 0111 7 7 ∣ \def\arraystretch{1.5} \begin{vmatrix} B & U & T \\ \hline 0100 & 4 & 4 \\ 0101 & 5 & 5 \\ 0110 & 6 & 6 \\ 0111 & 7 & 7 \\ \hdashline \end{vmatrix} B0100010101100111U4567T4567 ∣ B U T 1000 8 − 8 1001 9 − 7 1010 10 − 6 1011 11 − 5 ∣ \def\arraystretch{1.5} \begin{vmatrix} B & U & T \\ \hline 1000 & 8 & -8 \\ 1001 & 9 & -7 \\ 1010 & 10 & -6 \\ 1011 & 11 & -5 \\ \hdashline \end{vmatrix} B1000100110101011U891011T−8−7−6−5 ∣ B U T 1100 12 − 4 1101 13 − 3 1110 14 − 2 1111 15 − 1 ∣ \def\arraystretch{1.5} \begin{vmatrix} B & U & T \\ \hline 1100 & 12 & -4 \\ 1101 & 13 & -3 \\ 1110 & 14& -2 \\ 1111 & 15 & -1 \\ \hdashline \end{vmatrix} B1100110111101111U12131415T−4−3−2−1
那么假设现在有一种极端情况,当out=2^{32}-1
,而in=0
时,此时队列已用空间为1。我们知道in-out+2^{32}=1=len
,但计算机如何表示的呢?把4bit的规律扩展到32bit下,那么和负数相对应的最小无符号数不是
2
31
2^{31}
231吗?
其实这是一种溢出机制。仍旧以4bit表示为例,当我们用0-15
时,结果为-15
,超出了4bit表示范围,此时需要5bit或者说8bit表示,因为计算机量级总是按照2的整数次幂增长,那么在5bit下
−
15
=
1
0001
‾
-15=1\underline{0001}
−15=10001。这已经溢出,那么此时需要截取最后4bit作为结果,也就是1。
同理,把情况扩展到32bit下相信读者也能很快理解。当我们需要的结果不在有符号32bit表示的正数范围时,其实一种溢出并取低位的结果。
总结
希望在读者看来讲的还算清楚,如有问题欢迎批评指正。希望大家有所收获。