0%

八位数码管+流水灯(BSP版本)

因为不是专业的单片机专家,所以既然提供了带有BSP库的版本,自然就用BSP版本。这里介绍一下BSP版本的代码。

文件说明

image-20220629151028369

文件的主题部分如上图,除了main函数以外,我们看到STC15F2K60S2.H和前面一样是端口的地址定义的头文件

然后是displayer.h头文件,

image-20220629151443888

这一部分就是显示功能的相关接口,STC单片机上的显示主要是数码管和LED灯。

  • DisplayerInit是显示模块初始化函数,会对对应的函数的硬件,以及相关针脚进行初始化,具体可以看看前面的博文,就是作P0、P2端口等以及其他相关寄存器的初始化
  • SetDisplayerArea是设置有效显示区域,根据[begin, end]设置数码管显示范围。如果begin = 0,end = 7,显示的就是所有的数码管。但是范围不仅仅是这样,如果你的end大于7,比如是255,这也是能够正常工作的,只是硬件地址只有0-7,但是其他的无效地址会消耗一定的时间,这样总体显示时间变长了,由于是动态扫描的,所以8个有效数码管显示会出现闪烁,亮度下降的现象。
  • Seg7Print参数是输入8个要显示的数据,这个可以根据输入的参数作为在decode_table[]中的索引,可以显示对应灯管的组合
  • LedPrint控制8个数码管开关,一个位对应一个数码管,1是亮,0是暗。

还有一个文件是sys.h,这个是用来执行系统级别的操作,旨在对于用户以及系统的事件做出响应

image-20220629155718409

image-20220629155747086

上面是系统相关的函数,注释写的很清楚,主要是初始化系统,并且定义了事件,以及响应事件的函数。这里不详细展开,用到的时候再进行说明。

上面头文件涉及的函数,在STCBSP_V3.6.LIB静态库中进行定义,因此可以直接使用。

image-20220629154235232

主函数逻辑说明

image-20220629160509448

main.c中需要将SysClock设置成和下载的IRC一样的频率

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/* 显示数码管和LED等需要的头文件,其中前面两个是必须的 */
#include "STC15F2K60S2.H" // 端口地址定义
#include "sys.H" // 使用系统的函数定义
#include "displayer.H" // 数码管显示

/* 必须。定义系统工作时钟频率(Hz),用户要修改成和实际工作频率(下载时)一致,如上图 */
code unsigned long SysClock=11059200; // code是告诉单片机这个变量的值刷写入ROM,不再改变

/* 用户可以进行自定义数码管显示,主要是通过decode_table */
#ifdef _displayer_H_
code char decode_table[]={ /* 值 */
0x3f, // 0
0x06, // 1
0x5b, // 2
0x4f, // 3
0x66, // 4
0x6d, // 5
0x7d, // 6
0x07, // 7
0x7f, // 8
0x6f, // 9
0x00, // 无
0x08, // 下-
0x40, // 中-
0x01, // 上-
0x41, // 上中-
0x48, // 中下-
0x3f|0x80, // 带小数点0
0x06|0x80, // 带小数点1
}; // 也可以采用上面的拼接方式进行段选信息的构造
#endif

/* LED显示函数,可回调 */
void my100mS_callback()
{
// 定义一个LED寄存器类型
unsigned char a;
// LED显示左移
if(a != 0) a=a<<1;
// 超出了,重置
else a=0x01;
// 显示寄存器值
LedPrint(a);
/**
* 这里仅仅进行一次打印操作
* 但是每一次a的值从上一次继承下来,实现了移位操作
* a是局部变量
* 从局部变量声明的时候,它就在堆栈空间了,而不是调用函数的时候,才让它入栈的。
*/
}
void main()
{
// 使用显示设备,需要进行初始化
DisplayerInit();
// 设置显示范围为0-64,实际有效只有0-7,因此有无效地址消耗时间,数码管会出现闪烁变暗
SetDisplayerArea(0,64);
// 八个数码管分别显示1-8
Seg7Print(1,2,3,4,5,6,7,8);
// 利用回调函数,每100ms,就调用一次函数,这么做是为了节省栈空间,因为单片机的栈很小
SetEventCallBack(enumEventSys100mS, my100mS_callback);
/* 这里是初始化系统并循环等待事件,如果没有下面这部分,程序运行结束,板子就死了 */
MySTC_Init();
while(1) {
MySTC_OS();
}
}

C语言版本

整体和上一个版本差不多,这里不做赘述。

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/**********************
:八位数码管+流水灯
型号:STC15F2K60S2 主频:11.0592MHz
************************/
#include <STC15F2K60S2.h>
#define uint unsigned int
#define uchar unsigned char

uchar arrSeg7Select[] = {0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f}; //显示0-8
uchar arrDigitSelect[] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}; //数码管0-7

/*---------引脚别名定义---------*/
sbit sbtLedSel = P2 ^ 3; //数码管与LED灯切换引脚

/*---------变量定义---------*/
uchar uiLed = 0x01; //LED灯值寄存
uint uiLedCnt = 0; //LED灯累计计数器
uchar i = 0; //数码管扫描显示循环

/*---------初始化函数---------*/
void Init()
{
P0M1 = 0x00;
P0M0 = 0xff;
P2M1 = 0x00;
P2M0 = 0x08;
sbtLedSel = 0; //先选择数码管亮
}

/*---------延时函数---------*/
//下为生成1ms的延时函数,通过传入参数n,函数可以延时n毫秒
void delay_ms( uint n )
{
while( n )
{
uchar i, j;
i = 11;
j = 190;
do
{
while ( --j );
}
while ( --i );
n--;
}
}

/*---------主函数---------*/
void main()
{
Init();
while( 1 )
{
sbtLedSel = 0;
for( i = 0; i < 8; i++ )
{
P0 = 0;
P2 = arrDigitSelect[i]; //选择数码管的位数
P0 = arrSeg7Select[i + 1]; //显示对应的数值
delay_ms( 1 );
}
uiLedCnt++;
sbtLedSel = 1;
P0 = uiLed; //LED显示
delay_ms( 1 ); //延时200ms
if( uiLedCnt == 50 )
{
if( uiLed == 0x80 ) //value等于0x80时,重新赋初值0x01
uiLed = 0x01;
else
uiLed = uiLed << 1; //value值逐一左移
uiLedCnt = 0;
}
}
}

扫描频率可以改变的电子钟

程序设计思路

数字钟通过计数模拟时钟,将计数值,转换成时间格式,以格式:“时-分-秒”在LED数码管上显示,并通过案件调整扫描频率。

img

相关寄存器设置

  • P0的八个位和P2.3设置成推挽输出。按键是输入,不需要推挽。设置寄存器配置值如下。

    P2以及P0的端口设置,和上一个数码管扫描实验是一样的。

    额外增加的考虑是P3端口的设置。0通道和1通道都是0,是传统的8051I/O口模式。

    1
    2
    3
    4
    5
    6
    P2M1=0x00;
    P2M0=0xff;
    P0M1=0x00;
    P0M0=0xff;
    P3M0=0x00;
    P3M1=0x00;
  • 通过定时器0,采集方式1,在定时器中断中进行计数累加

    1
    TMOD = 0x01;
  • 打开总的中断

    1
    EA = 1;
  • 开启定时器中断

    1
    ET0 = 1;
  • 计数寄存器初始化

    定时器0设置于模式1时,计数寄存器为16位模式,由高8位TH0和低8位TL0两个8位寄存器组成,当设定计算值为65536-50000=15536(D)时,转换为十六进制就是3CB0(H),此时,TH0=3C,TL0=B0分别装入即可,为了免除这些计算步骤,很多编程者采用“TH0=(65536-50000)/256;TL0=(65536-50000)%256“的编程方式,去让单片机自己去计算结果,那么为什么要介入256呢?其实并不难理解,做一下10——16进制的换算就知道了,256(D)=0100(H),这里01就是高8位的数据,00就是低8位的数据,通俗点说,15536(D)里有多少个256,就相当于高8位有多少数值,就是除的关系了,商存入高8位寄存器后余下的数存入低8位即可,取商计算就是TH0=(65536-50000)/256;而取余计算就是TL0=(65536-50000)%256 。

    1
    2
    TH0=(65535-1000)/256;
    TL0=(65535-1000)%256;
  • 启动定时器

    1
    TR0=1;
  • 中断1优先级置1,表示中断1设置为最高优先级

    1
    PT0=1;
  • 设置P2和P0为推挽输出模式

    1
    2
    3
    4
    P2M1=0x00;
    P2M0=0xff;
    P0M1=0x00;
    P0M0=0xff;

代码说明

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
/* 引入端口定义头文件和中断定义头文件 */ 
#include "STC15F2K60S2.h"
#include "intrins.h"

/* 重命名,简写 */
#define uint unsigned int
#define uchar unsigned char

/* 中断1 */
#define i1 interrupt 1

sbit ledSel = P2 ^3; // LED或数码管选择信号(共用选择端信号)
sbit key1 = P3 ^2; // 按键控制数码管扫描的频率

/* 显示0-9对应的数字,段选选择 */
uchar baseSevenSegment[] = {
0x3f, 0x06, 0x5b, 0x4f, 0x66,
0x6d, 0x7d, 0x07, 0x7f, 0x6f
};
unsigned char const line = 0x40; // "-":8中中间横杠的段选信息
char timeAddOneFlag = 0; // 时间增加标志,为1,时间增加一秒
char key1ActionFlag = 0; // 按键操作标志,为1,按键操作需要响应
unsigned int ledOnFlag = 0; // LED亮起标志,为1,LED亮起
char tubeOnFlag = 0; // 数码管亮标志,为1,数码管亮起
int ledValue = 1; // LED显示值为多少
int myDisplay[8] = {0}; // 数码管显示值为多少,一共8个数码管
unsigned int timeCount = 1; // 计数器
unsigned int currHour = 0; // 当前小时
unsigned int currMinute = 0; // 当前分钟
unsigned int currSecond = 0; // 当前秒钟
unsigned int interruptCount = 0; // 中断计数器
unsigned int keyDownTime = 0; // 按键按下的时间
unsigned int scanTime = 1; // 扫描时间
unsigned int currBit = 0; // 当前显示的位

/* 延时函数 可以用ISP生成 */
void Delay5us() //@11.0592MHz
{
unsigned char i;
_nop_();
i = 11;
while (--i);
}

/* 设置ledSel,转为数码管显示 */
void switchToTube() {
ledSel = 0;
}

/* 设置ledSel,转为led设置 */
void switchToLed() {
P0 = 0;
ledSel = 1;
}

/**
* 改变其中一位的内容
*
* @param bitNum 位数(第几位)(例如最左那位,则调用1)
* @param value 改变后的数字,需确保大于0小于10
*/
void change1Bit(int bitNum, int value) {
myDisplay[bitNum - 1] = baseSevenSegment[value];
}

/**
* 改变其中一位的内容(设置七段码)
*
* @param bitNum 位数(第几位)(例如最左那位,则调用1)
* @param sevenSegCode 目标七段码
*/
void change1Bit_seven(int bitNum, int sevenSegCode) {
myDisplay[bitNum - 1] = sevenSegCode;
}

/**
* 数码管设置为显示指定的数值
*
* @param num 数字
*/
void changeAll(long num) {
int i;
for (i = 7; i >= 0; --i) {
int foo = num % 10;
myDisplay[i] = baseSevenSegment[foo];
num /= 10;
}
}

/**
* led亮
*/
void displayLed() {
if (ledOnFlag) {
switchToLed();
P0 = ledValue;
}
}

/**
* 显示数码管
*/
void showTube() {
if (tubeOnFlag) {
switchToTube();
P0 = 0;
P2 = currBit;
P0 = myDisplay[currBit];
Delay5us();
}
}

/**
* 初始化定时器
*/
void timer0Initialize() //0.1毫秒@12MHz
{
AUXR |= 0x80; // 定时器时钟1T模式
TMOD &= 0xF0; // 设置定时器模式
TL0 = 0xAE; // 设置定时器初值
TH0 = 0xFB; // 设置定时器初值
TF0 = 0; // 清除TF0标志
TR0 = 1; // 定时器0开始计时
EA = 1; // 打开总的中断
ET0 = 1; // 打开定时器0中断
}

/**
* 整体程序初始化函数
*/
void initialize() {
P0M0 = 0xFF;
P0M1 = 0x00;
P2M0 = 0x0f; //设置P2.0-3为推挽工作状态
P2M1 = 0x00;
P3M0 = 0x00;
P3M1 = 0x00;
P3M0 = 0x00;
P3M1 = 0x00;
ledSel = 0;

timer0Initialize();
changeAll(0);
/*
* 第三位和第六位设置为 '-'
*/
change1Bit_seven(3, line);
change1Bit_seven(6, line);
}

/**
* 时间自增1秒
*/
void addTime() {
++currSecond;
if (currSecond == 60) {
++currMinute;
currSecond = 0;
if (currMinute == 60) {
++currHour;
currMinute = 0;
if (currHour == 24)
currHour = 0;
change1Bit(2, currHour % 10);
change1Bit(1, currHour / 10);
}
change1Bit(5, currMinute % 10);
change1Bit(4, currMinute / 10);
}
change1Bit(8, currSecond % 10);
change1Bit(7, currSecond / 10);
}

/**
* 按下key1要做的事
*/
void key1Action() {
if (key1ActionFlag) {
if (ledValue == 0x80)
ledValue = 0x01;
else
ledValue <<= 1;
if (ledValue == 1)
scanTime = 1;
else if (ledValue == 2)
scanTime = 50;
else if (ledValue == 4)
scanTime = 100;
else if (ledValue == 8)
scanTime = 200;
else if (ledValue == 16)
scanTime = 500;
else if (ledValue == 32)
scanTime = 1000;
else if (ledValue == 64)
scanTime = 2000;
else if (ledValue == 128)
scanTime = 5000;
key1ActionFlag = 0;
}
}

/**
* 收到中断的信号(是时候自增时间了)
*/
void timeSignalHandler() {
if (timeAddOneFlag) {
addTime();
timeAddOneFlag = 0;
}
}

/**
* 单片机运行
*/
void run() {
while (1) {
timeSignalHandler();
displayLed();
showTube();
key1Action();
}
}

/**
* 每0.1毫秒进入一次定时器中断
*/
void interruptFunction() i1 {
static const int KEY_TIME_THRESHOLD = 500;
interruptCount = (interruptCount + 1) % 20000;
// led显示的频率应该低些才有好的显示效果
if (interruptCount % 12 > 10) {
ledOnFlag = 1;
tubeOnFlag = 0;
} else {
tubeOnFlag = 1;
ledOnFlag = 0;
}
// 数码管的扫描
if (interruptCount % scanTime == 0)
currBit = (currBit + 1) % 8;
// 读秒
if (interruptCount % 10000 == 0)
timeAddOneFlag = 1;
// 按键功能设置
if (key1 == 0) {
if (keyDownTime < KEY_TIME_THRESHOLD)
++keyDownTime;
} else {
if (keyDownTime >= KEY_TIME_THRESHOLD)
key1ActionFlag = 1;
keyDownTime = 0;
}
}

int main() {
initialize();
run();
return 0;
}

八位数码管动态扫描

实验工程可以在学习通上面下载

程序设计流程图

img

在程序启动的初期,需要对硬件有抽象的定义,这由我们的.h头文件表示,初始化硬件,实际是对一些寄存器的初始化,和上一个流水灯的init函数差不多。然后就是主要逻辑,扫描位选,并完成端口的赋值。

原理说明

什么是段选和位选

简单说:段选信号是选择数码管的那一段灯管亮起,位选信号是选择八个数码管的哪一个。

image-20220627143830272

如下图,由于这八个数码管是通用的同一个GND,也就是共地的。(一次只能一个开漏地级接给一个数码管,否则不同的二极管会冲突)因此,一次只能有一个数码管亮起,比如下图中八个数码管一次显示0-7,则只能够从左到右,D1先显示0,D1熄灭,D2再显示1,D2熄灭,…,然后轮流下去,如果速度够快,人眼看起来就像灯是一起亮着的。

image-20220627135806124

段选进一步说明

image-20220627142752484

image-20220627142912078

位选就是第几个数码管,段选是怎么来的?

比如我们要左起第3个(01234567)数码管显示6,因此位选就是2,也就是P2 = 2,当然也可以用程序中的方法(多此一举)P2 = weixuan[2]

然后段选要显示6根据上面的图,我们知道,显示6,需要“a, c, d, f, g, e”这些灯管亮起。所以位选信息如下

image-20220627144325373

所以因此段选信息就是:duanxuan[6] = 0b0111101 = 0x7d

所以对应P0.7-P0.0就是0x7d

类似上面的方法就可以枚举出所有的段选情况:

1
uchar duanxuan[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f};

代码说明

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
42
43
44
45
46
47
48
/* 对硬件各个端口的定义的头文件,直接使用即可 */
#include <STC15F2K60S2.h>
/* 重定义类型名,简写 */
#define uint unsigned int
#define uchar unsigned char

uint i=0;
// 段选:选择亮灯
uchar duanxuan[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f};
// 位选:选择哪一个数码管
uchar weixuan[]={0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07};

void Delay(int n) // 延时函数,单位毫秒
{
while(n--);
}

void main()
{
/* 设置推挽输出
* 和流水灯实验类似,对P0端口和P2端口进行设置
* 端口设置推挽输出,通道0设置“1”,通道0全0,具体见数据手册,流水灯也讲过了
* 端口0的0通道,是表示位选,八个位对应八个灯管,所以全部设成1
* 端口2的0通道,是表示段选,一次只能选一个数码管,由三位输入进行选择
* 分别是:P2.2 P2.1 P2.0
* 刚好是P2的第三位,所以赋值时候,只要P2 = weixuan[i]
* 就可以
* 这里简单将2的0通道全部设成1
*/
P2M0=0xff;
P2M1=0x00;
P0M0=0xff;
P0M1=0x00;
/* 循环扫描数码管
* 不断扫描数码管,使每一小段数码管都点亮一段时间
* 视觉上一直是亮的状态
*/
while(1)
{
for(i=0;i<8;i++)
{
P0=0;
P2=weixuan[i]; // 选择数码管的位数
P0=duanxuan[i+1]; // 选择对应的数值
Delay(600); // 延时
}
}
}

流水灯实验

程序设计思路

流水灯是经典实验。要点亮发光二极管,要把P0端口和P2.3端口设置成推挽输出,然后

  • 将P2.3设成“1”,这表示使能发光二极管
  • 对P0端口赋值,就可以点亮对应的led灯了

而对于流水灯,基本思路就是点亮一个led灯,等待一段时间,然后熄灭它,同时点亮下一个led灯,然后如此循环下去,就可以看到流水效果如下

1
2
3
4
5
6
7
8
9
10
00000001
00000010
00000100
00001000
00010000
00100000
01000000
10000000
00000001
...

关键代码说明

相关定义以及头文件

1
2
3
4
5
6
7
8
9
10
11
/* 这个是我们的单片机的端口的定义 */
/* 如果仔细阅读头文件,可以看到就是用唯一的整数标识每一个端口 */
#include<STC15F2K60S2.H>

/* 定义类型,简写 */
#define uchar unsigned char
#define uint unsigned int

/* 选择位 */
sbit led_sel=P2^3; // 端口P2.3
uchar led; // 用一个uchar表示8个LED,每一个是1位

本程序主要是三个函数组成

对二级管初始化

void Init();

这个函数主要是发光二极管的初始化设置:

只要将P0口和P2.3工作模式设置为推挽输出,同时将P2.3置“1”,使能发光二极管电路。(这里的置“1”是P2的bit3置为1)

image-20220626175135866

image-20220626175208809

其中P0、P2这两个端口的设置通过对对应的寄存器赋值来设置实现。

看看上面两个表,其中第一列,也就是通道1为0(八个位都是0),通道0为1(特定位为1),表示处于推挽模式。STC15F2K60S2数据手册4.1章节(322页)有详细的介绍,下面简单介绍进一步如何配置。

image-20220626174347225

上面是STC-B的部分电路原理图。由于处于推挽输出模式,要求通道0为1,通道1为0。通道1为0,就把通道一八个位全部设置成低电平。但是通道0为1就要对特定位进行赋1。对于P0端口,我们可以在电路图中看到是红色框框,它对应了八个发光二极管的正极端,所以要让8个发光二极管统统发光,就要全部设置使能。所以对于P0的通道0,进行赋1,就要把八个位全部设置成1。另一方面,对于P2端口。我们看到上面对于这个实验的led灯,只有一个LED负极开漏端有一个LED_SEL选择信号,表示选择哪一个LED发光。可以看到蓝色框框,只是P2.3,也就是端口P2的0通道的第三个位赋1就好。

因此我们得到如下初始化代码:

1
2
3
4
5
6
7
8
9
10
11
12
void Init() {
// P0的通道1全0
P0M1=0x00;
// P0的通道0全1
P0M0=0xff;
// P2的通道1全0
P2M1=0x00;
// P2的通道0只要bit3为1
P2M0=0x08;
// 流水灯从L0开始(00000001)
led_sel=1;
}

延时函数

void delay_ms(uint n);

函数delay_ms的功能是延时n毫秒,但时间不一定特别准确。单片机工作在同一的时钟脉冲控制下。这个脉冲式单片机控制器的时钟电路产生的。时钟电路由振荡器和分频器构成,振荡器产生基本振荡信号,然后分频,得到相应时钟。(这玩意了解一下就好了)

关于单片机中周期的说明:

  • 振荡周期: 晶体振荡器的周期。

  • 状态周期: 振荡信号经二分频后形成的时钟脉冲信号,用S表示。一个状态周期的两个振荡周期作为两个节拍分别称为节拍P1和节拍P2。P1有效时,通常完成算术逻辑操作;P2有效时,一般进行内部寄存器之间的传输。

  • 机器周期: 完成一个基本操作所需的时间称为机器周期。一个机器周期包含6个状态周期,用S1、S2、….、S6表示;共12个节拍,依次可表示为S1P1、S1P2、S2P1、S2P2、……、S6P1、S6P2。

  • 指令周期:CPU执行一条指令所需要的时间。CPU执行指令是在时钟脉冲控制下一步一步进行的,由于指令的功能和长短各不相同,因此,指令执行所需的时间也不一样。一个指令周期通常含有1~4个机器周期。

img

上图是MCS-51单片机各种周期的相互关系。

因此,根据指令执行的时间,可计算出1ms可以相应执行多少条指令,函数中可通过循环执行空指令来达到延时1ms的效果。此外延时函数也可以在STC-ISP中通过“软件延时计算器”功能自动生成指定延时时间的延时函数代码,如下:

image-20220626184218824

直接用ISP串口助手生成延时200ms对应的代码即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void Delay200ms()		//@11.0592MHz
{
unsigned char i, j, k;
//_nop_();
//_nop_();
i = 9;
j = 104;
k = 139;
do
{
do
{
while (--k);
} while (--j);
} while (--i);
}

主函数

void main();

首先要调用函数Init()对电路进行初始化,再循环地对P0口进行赋值,点亮流水灯。

每一次流水灯对应点亮的位向左移动一位,具体就如“程序设计思路”的演示图所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void main()
{
// 初始化LED灯
Init();
// 从L0开始亮
led=0x01;
// 无休止闪烁
while(1)
{
// P0端口连接LED灯,所以把寄存器LED给P0
P0=led;
// 延时200ms
Delay200ms();
// LED寄存器状态赋值
if(led==0x80) // 如果到了L7灯,下一个状态又从L0开始
led=0x01;
else // 否则左移一位
led=led<<1;
}

}

下载程序

在学习资料里面下载相关工具。主要是keil uvision 4

image-20220627112449125

新建工程uprojx,然后编译生成hex文件,烧入程序,运行。

如果编译没有生成.hex文件可以进行如下设置

image-20220627112712761

成功效果如下

image-20220627112808166

人流统计模块讲解

我们使用onenet提供的AI平台的接口

人体检测

能力介绍

接口能力:检测并定位图片中无遮挡的人体,返回人体位置和置信度(0~1),可实现多人体检测;

图片格式:现支持PNG、JPG、JPEG、BMP,不支持GIF图片;

图片大小:上传图片大小不超过2M;图片中识别的人体不被遮挡,人体区域高度120像素以上,宽度80像素以上;

业务应用:智慧零售人流量统计、人体跟踪、安防监控。

API调用方式

请求方式 POST
url http://ai.heclouds.com:9090/v1/aiApi/picture/BODY_RECO
http-header token: xxxxxxxxxxxxxxxxx //通过AI Key和Secret Key鉴权(推荐)
request-body { "picture": ["String"] //一张图片的base64图片编码

下面是python的示例代码

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
import requests
import json
import base64

url = 'http://ai.heclouds.com:9090/v1/aiApi/picture/BODY_RECO'
headers ={
'Content-Type':'application/json',
'token':'xxxxxxxxxxxxxxxxx(用户鉴权接口返回结果中的accessToken)'
}


# 打开图片文件
file = open('D:/Desktop/体验图片/03人体检测/body5.png','rb')

# 将其转为base64信息
base64Str = base64.b64encode(file.read()).decode()

# 关闭打开的文件
file.close()

# 构造接口调用参数
data = {
'picture':[base64Str]
}

# POST 方式调用
response = requests.request("POST", url, headers=headers, data=json.dumps(data))

# 打印结果
print(response.text)

返回示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"stateCode": "0x0000", //结果状态码,16进制。"0x0000":成功
"message": "success", //返回信息
"data": Array //JSON对象数组,每一个JSON对象表示一个人体,包含了人体在图片中的位置、大小,标签,置信度
}
例如:
[
{
//人体在图片中的位置、大小
"box": {
"x": 50,
"y": 270,
"width": 795,
"height": 978
},
//置信度(0~1)
"confidence": 0.9998502731323242,
//标签
"label": "person"
}
]
注意:
http错误码返回"401"时表示"未经授权",造成的原因有:未使用或使用的token不正确;使用的token已经超时失效。

代码说明

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
42
43
44
45
46
47
import requests
import json
import base64

# 获得tocken对应的keys
aiKey = 'd071c2afa0f14fb28d40be114e524984'
secretKey = '209e5e8fb0b64299b671cc8a0c3a3592'



# 获取access Token
def get_access_token(aikey, seckey):
tkUrl = 'http://ai.heclouds.com:9090/v1/user/app/accessToken?aiKey=%s&secretKey=%s' % (aiKey, secretKey)
tkheader = {
"Content-Type" : "application/json"
}
# 获得tocken返回的json
r = requests.get(url=tkUrl, headers=tkheader).json()
return r["data"]["accessToken"]



def getRes():
url = 'http://ai.heclouds.com:9090/v1/aiApi/picture/BODY_RECO'
headers ={
'Content-Type':'application/json',
'token': get_access_token(aiKey, secretKey)
}
# 打开图片文件
file = open('1.png','rb')
# 将其转为base64信息
base64Str = base64.b64encode(file.read()).decode()
# 关闭打开的文件
file.close()
# 构造接口调用参数
data = {
'picture':[base64Str]
}
# POST 方式调用
response = requests.request("POST", url, headers=headers, data=json.dumps(data)).json()
# 打印结果
# print(len(response["data"]))
return len(response["data"])


if __name__ == '__main__':
getRes()

注释已经讲解的很清楚了。上一个函数是获得accessToken的函数,下一个就是获取答案,由于答案是返回一个json数组,每一个元素就是一个人体,一个框框的信息。我们直接次len函数获得数组的元素个数。我们就可以获得实时人数。

操作系统铧锺碘

期末考占总评的40%

老师提到的考试复习指南:

  • 注重应用,需要直到API怎么用,各个参数是什么
  • 能够举一些例子
  • 从网络上找一些经典的资料

四个简答题

分值:5分一个

进程

  • 各种调度算法是什么样的。各种调度算法有什么特点,还存在什么问题?
  • 多级反馈队列为什么常用,彩票调度算法和步长调度算法等公平份额算法为什么不行?

各种调度算法的特点和优缺点

先进先出(FIFO)
  • 先进先出(First In First Out,FIFO),或者又叫做先到先服务(First Come First Served,FCFS)

image-20220614133547462

  • 优点:简单,易于实现(最符合直觉的策略)
  • 存在问题:
    • 存在护航效应(一些耗时较少的潜在资源消费 者被排在重量级的资源消费者之后)。简而言之就是:先到达的长进程让后到达的短进程饿死。
    • 周转时间和响应时间都是糟糕的。
最短任务(作业)优先(SJF)
  • 最短任务优先(First Job First,SJF):先运行最短的任务,然后是次短的任务,如此下去。

image-20220614133837340

  • 特点:
    • SJF在所有作业同时到达的情况下是最优(optmial,opt)调度算法。
    • 它是非抢占式的,如果长任务比短任务先到达,还是会有护航效应。
  • 优点:
    • 周转时间很好
  • 缺点:
    • 响应时间不好
最短完成时间优先(STCF)
  • 向SJF添加抢占,就是最短完成时间优先(Shortest Time-to-Completion First,STCF)或者称作抢占式最短作业优先(Preemptive Shortest Job)调度程序

image-20220614135705282

  • 特点:
    • STCF在作业不同时到达的情况下延续了SJF的最优性
    • 它是抢占式的,如果长任务先到达,短任务后到达,短任务会抢占CPU先执行。
  • 优点:
    • 周转时间很好
  • 缺点:
    • 响应时间不好
轮转
  • 轮转(Round-Robin,RR),又称作时间切片(time-slicing):RR在一个时间片(time slice,有时称为调度量子,scheduling quantum)内工作,然后切换到运行队列的下一个任务,而不是运行一个任务直到结束。它反复运行,直到所有任务完成。

image-20220614140940324

  • 时间片长度影响性能:越短,响应时间越好;太短,频繁切换上下文影响整体性能。
  • 特点:是一种公平调度算法
  • 优点:
    • 响应时间好
  • 缺点:
    • 周转时间差
  • 进一步优化:重叠RR,考虑了IO,需要IO的任务让出CPU在完成前不参与调度。
多级反馈队列(MLFQ)
  • 规则:期中考过了,我觉得不会再考,但是仍然很重要。
    • 规则1:如果A的优先级>B的优先级,运行A(不运行B)。
    • 规则2:如果A的优先级=B的优先级,轮转运行A和B。
    • 规则3:工作进入系统时,放在最高优先级(最上层队列)。
    • 规则4:一旦工作用完了在某一层中的时间配额(无论中间主动放弃多少次CPU),就降低优先级(移入低一级队列)。
    • 规则5:经过一段时间S,就将系统中所有工作重新加入最高优先级队列。
  • 特点:
    • 在没有任务长短的先验知识情况下,同时优化了周转时间和响应时间。
    • 采用多级队列,每个队列代表一个优先级。
  • 优点:
    • 对于短时间运行的交互型工作,获得类似SJF/STCF的很好的全局性能(周转时间)。
    • 同时对长时间运行的CPU密集型负载也可以公平地运行(响应时间)。
彩票调度(Lottery)
  • 给每个进程分发⼀定量的彩票,每次调 度时随机抽出⼀个号码,被抽中的进程获得CPU。
  • 彩票数(tickets)代表了进程占有某个资源地份额。一个进程拥有的彩票占总数地百分比,就是占有资源的份额。

image-20220614142733606

  • 特点:
    • 充分利用了随机性
    • 从概率上满足期望的比例,但不能确保。运行时间越长,得到的CPU时间比例越接近期望。
  • 优点:
    • 可以避免奇怪的边角情况。
    • 很轻量,几乎不需要记录任何状态
    • 随机方法很快
  • 缺点:
    • 工作时间很短的时,平均不公平度很差。只有运行很多时间片时,才得到期望结果。
步长调度算法(Stride)
  • 给每个进程分发⼀定量的彩票,用一个大数除以彩票得到步长,程序被调度⼀次则累计⼀次行程值(pass值 += 步长),每次调度行程值最小的进程。

image-20220614144327579

  • 优点:在给定优先级的情况下,达到绝对公平,比彩票调度优化了确定性。
  • 缺点:需要一个全局状态(pass值)

为什么MLFQ可用,彩票步长不用

  • MLFQ可用的原因就是它的优点:

    • 对于短时间运行的交互型工作,获得类似SJF/STCF的很好的全局性能(周转时间)。
    • 同时对长时间运行的CPU密集型负载也可以公平地运行。
  • 彩票步长不广泛使用的原因:

    • 一个原因是这两种方式都不能很好地适合I/O;
    • 另一个原因是其中最难的票数分配问题并没有确定的解决方式。

虚拟内存

  • 虚拟内存空间是什么?
  • 为什么需要虚拟内存空间而不是直接使用物理内存空间?
  • 使用虚拟内存空间的目标或者说是指标是什么?

什么时虚拟内存空间?

虚拟内存是计算机系统内存管理的一种技术。它负责为程序提供一 个巨大的、稀疏的、私有的地址空间的假象,其中保存了程序的所有指令和数据。而实际上,它通常是被分隔成多个物理内存片段,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。

为什么需要虚拟内存空间?

主要是虚拟内存提供了三个主要的好处:易于使用,高性能,可靠。

  • 虚拟内存可以为进程提供独立的内存空间并引入多层的页表结构将虚拟内存翻译成物理内存,进程之间可以共享物理内存减少开销,也能简化程序的链接、装载以及内存分配过程;
  • 虚拟内存可以结合磁盘和物理内存的优势为进程提供看起来速度足够快并且容量足够大的存储;
  • 虚拟内存可以控制进程对物理内存的访问,隔离不同进程的访问权限,提高系统的安全性;

虚拟内存的目标(指标)

image-20220615010114930

image-20220615010135694

并发

  • 什么是并发?并发的概念是什么?
  • 竞态条件、临界区等概念

什么是并发

并发当有多个线程在操作时,如果系统只有一个CPU,则它不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。这种方式我们称之为并发。

什么是临界区、竞态条件等

image-20220614155546936

持久

明确为:崩溃一致性

  • 什么是崩溃一致性
  • 崩溃一致性存在什么例子

什么是崩溃一致性问题

文件系统如何在出现断电(power loss)或系统崩溃(system crash)的情况下,更新持久数据结构。这称为崩溃一致性问题。

存在什么例子

  • 课本42.1

简单描述就是:

image-20220615161654399

考虑一种情况是将单个数据块附加到原有文件末尾。则这个过程文件需要更新的有:数据位图增加一个表示数据块有效的位;更新inode中的数据块指针等;更新数据块Db。

现在我们考虑6种不一致现象:

  • 只更新了数据块。此时,该文件的inode中没有指向Db的指针。Db的写入相当于无效。
  • 只更新了inode此时,数据块没有更新,导致读取这个块返回给用户垃圾数据。同时出现文件系统不一致,数据位图告诉我们Db这个块未分配,但是inode认为它分配了。
  • 只更新了数据位图。此时,同样出现文件系统不一致,数据位图认为Db这个块分配了,但是inode没有指向它。如果不解决,会造成空间泄露。
  • 只更新了inode和数据位图。此时文件系统的元数据是一致的。但是数据没有更新,造成了垃圾数据。
  • 只更新了inode和数据块。inode能够指向正确的数据块。但是又出现了文件系统给元数据不一致(inode和位图)。
  • 只更新了数据位图和数据块。inode和位图的不一致性问题。没有inode指向这个块。

五个计算题加分析题

共计80分,其中由于计科期中考试进程考的比较多,进程部分的分值会少一点

五个答题主要是1+2+1+1:进程一个,虚拟内存两个,并发一个,文件部分一个

进程x1

没给提示。有可能是fork和exec相关的。

  • (12 分)描述进程与线程以及它们之间的区别和联系。分析给出下图代码(图 5-1)在控 制台可能输出的信息;代码(图 5-2)可能会创建多少个进程,多少个线程。(答案5,2)

image-20220615163218702

image-20220615163231423

img

虚拟内存x2

有两个题,由于现代操作系统普遍使用分页管理机制,而不是分段管理机制,所以着重在分页上(自己体会这句话)。而且他不怎么说分段,所以肯定考分页。

  • 首先要搞懂最简单的线性页表,它有什么缺陷?
  • 页表使得访存需要增加额外的访问页表的成本,所以很慢。为此有了TLB加速缓存。TLB是怎么工作的。
  • 线性页表(一级)存储太大,所以有了多级页表。搞清楚怎么做的特别是二级三级页表。(我觉得是三级页表,老师提到了两三次)
  • 在页表中除了物理页号,还有一些其他的属性位,都有些什么,它们代表什么含义?(例如有效位、存在位等)
  • 通过上面的页表机制,给定一个虚拟地址,怎么计算它的物理地址?
  • 超越物理内存(置换策略)
  • 线性页表(linear page table),就是一个数组。操作系统通过虚拟页号(VPN)检索该数组,并在该索引处查找页表项(PTE),以便找到期望的物理帧号(PFN)。

  • 线性页表存在的问题就是太慢和太大

    • 对应的解决办法就是TLB和分级页表
  • TLB和多级页表很重要,但是请见书本。

  • 一些属性位:

    image-20220614162257586

    上图包含一个存在位(P),确定是否允 许写入该页面的读/写位(R/W) 确定用户模式进程是否可以访问该页面的用户/超级用户位 (U/S),有几位(PWT、PCD、PAT 和 G)确定硬件缓存如何为这些页面工作,一个访问位 (A)和一个脏位(D),最后是页帧号(PFN)本身。

    • 有效位(V):用于指定特定的地址转换是否有效
    • 保护位(R/W):表明页是否可以读取、写入或执行
    • 存在位(P):表示该页是在物理存储器上还是在磁盘交换区上
    • 参考位(A):又称访问位,用于追踪页是否被访问,也用于确定哪些页很受欢迎,应该保留在内存中。
    • 脏位(D):标示该页是否被写过
  • 怎么用页表进行地址转换请看书

    • 不难,就是查表
    • 需要注意,物理页(帧)号拼上页偏移才是物理地址。
  • 超越物理内存(置换策略)

    • 平均内存访问时间(Average Memory Access Time,AMAT)

    • 最优策略是MIN(无法实现,作为策略追求的性能上界)

    • 简单策略FIFO

      image-20220614163007021

    • 随机策略

      image-20220614163049148

    • 最近最少使用(Least-Recently-Used,LRU)

      image-20220614163141209

    • 最不经常使用”(Least-Frequently-Used, LFU)(历史信息中使用频次最低的)

  • image-20220615021516526

    • 基本思想:
      • 分段是指将虚拟地址空间中代码、堆、栈等段分别映射到物理内存,而不是将整 个虚拟地址空间全部映射到物理内存。
      • 分页是将虚拟地址空间等分为固定大小的页 (如 4KB),以页的方式将虚拟地址空间与物理内存进行映射。
    • 地址转换过程:
      • 分段主要是把不同的段,借助两个寄存器(基址寄存器和界限寄存器),其中物理地址等同于基址寄存器中存储的段机制加上段内偏移。界限寄存器主要实现了段的保护功能。
      • 页的地址转换是将虚拟地址分成虚拟页号部分和页偏移部分。虚拟页号通过页表存储的物理帧号实现虚拟页号到物理页号的映射。由于偏移不变,所以把得到的物理页号拼上偏移就是物理地址。
    • 优缺点:
      • 分段的优点是硬件支 持比较简单;缺点是可能会导致外部碎片、空闲空间的管理比较复杂。
      • 分页的优点是 空间管理简单、没有外部碎片,缺点是页表存储空间大、地址翻译慢(需要 TLB 加 速),因此需要硬件的支持较多。

并发x1

由于并发的计算分析大题只考一个,而且老师又提示说会考一提编程相关的代码题,要会用函数,会用接口。肯定就是这题。

用信号量解决理发师问题。如果有余力也可以看看信号量解决吸烟者问题。我觉得就是理发师

类似这个历年真题。把打印员换成理发师,就是经典的理发师问题。

image-20220615151151214

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
42
43
44
45
46
47
48
49
#define NUM_CHAIR 5					// 最大等待椅子数目
#define NUM_CUST 10 // 顾客数量
sem_t customers; // 表示有顾客的条件变量
sem_t barber; // 表示理发师醒了的条件变量
sem_t mutex; // 空闲椅子数是共享资源,需要互斥量
int freechairs; // 空椅子的数量

void * Barber() {
while (ture) { // 理发师持续不断工作或者睡眠
sem_wait(&customers); // 试图为一个顾客理发,如果没有顾客就睡着
sem_wait(&mutex); // 需要修改空椅子的数量(全局变量),需要上锁
freechair++; // 为一个顾客理发,顾客起身,空一张椅子出来
sem_post(&barber); // 此时理发师可以工作,通知顾客过来理发
sem_post(&mutex); // 修改完空椅子数目,释放锁
/* 理发师在理发... */
}
}

void * Customers() { // 一个线程仅仅表示一名顾客
sem_wait(&mutex); // 想要坐到一张椅子上等待
if (freechairs > 0) { // 如果还有空椅子的话
freechairs--; // 顾客坐到一张椅子上等待
sem_post(&customers); // 唤醒理发师,有顾客来了
sem_post(&mutex); // 顾客已经坐到椅子上等待,释放互斥量
sem_wait(&barber); // 如果理发师还在忙,就等他
/* 抢到理发师,此时顾客开始理发 */
}
else { // 没有空着的椅子
sem_post(&mutex); // 不要忘记试图坐下上的锁(第20行)
/* 没有椅子坐,直接走了 */
}
}

int main() {
/* 初始化 */
sem_init(&customers, 0, 0);
sem_init(&barbers, 0, 0);
sem_init(&mutex, 0, 1);
freechairs = NUM_CHAIR;

/* 启动理发师和顾客 */
pthread_t bar, cust[NUM_CUST];
Pthread_create(&bar, NULL, Barber, NULL);
for (int i = 0; i < NUM_CUST; i++) {
Pthread_create(&cust[i], NULL, Customers, NULL);
sleep(randint(NULL)); // 每隔一个随机时间来一个顾客
}
return 0;
}

补充之吸烟者问题

这里只用到了信号量,因为座子上的资源只够一个人吸烟,不会有共享

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

文件x1

  • 存储管理
    • 磁盘结构是什么样的?旋转寻道传输以及这方面的知识以及有关的时间是怎么算的?
    • RAID(0/1/4/5)各自有什么特点和优缺点,稳态吞吐量怎么算的
  • 文件系统
    • 数据进行读写,要经历什么样的步骤
    • 元数据的成员有哪些:修改时间、…、直接指针、间接指针
      • 放在一个应用场景中让你进行设计(大概率是设计分配直接指针和间接指针[二级、三级])
      • 对应消耗多少空间,对应的数据块又有多大?

磁盘结构

image-20220614164632169

image-20220615015619442

各种时间怎么算

有两个真题,可以理解一下

  • 真题1

image-20220615171059400

image-20220615171145581

  • 真题2:

image-20220615171003028

image-20220615170926641

RAID各级的优缺点以及特点

  • 【RAID 0——RAID中速度最快】

    原理:采用数据条带技术(Striping),将数据分成“块”保存在不同磁盘上,读写时以并行的方式对各磁盘同时进行操作。

    优点:组建低成本,且具有传输速度快、存储空间利用率高等优点;

    缺点:不提供数据冗余保护,任何一块硬盘发生损坏,则所有的数据将不可恢复;

  • 【RAID 1——RAID中安全性最高】

    原理:俗称“磁盘镜像”,将数据完全一致地分别写到工作磁盘和镜像磁盘,一旦工作磁盘发生故障,系统自动从镜像磁盘读取数据,不会影响用户工作。

    优点:磁盘数据呈现完全镜像,具有安全性好、技术简单、管理方便等优点;

    缺点:实现成本高,磁盘空间利用率仅 50%;

  • 【RAID 4——条带分布+专用盘校验】

    原理:其中一块硬盘上存储校验数据,当某块硬盘出现故障时,其它硬盘可以通过校验数据将有故障的硬盘的数据重新恢复出来。

    优点:1、磁盘利用率较高(N-1);2、并行I/O传输,顺序读性能较高

    缺点:1、专用校验盘成为性能瓶颈;2、每次读写牵动整个组,每次只能完成一次I/O 传输

  • 【RAID 5——RAID中综合性能最佳】

    原理:采用校验码冗余数据,校验块旋转地分布在多个磁盘上,当某个磁盘出现故障,可以使用其他磁盘上校验信息来恢复数据。

    优点:兼顾存储性能、数据安全和存储成本等;

    缺点:1、写入性能相对低;2、重建数据时,性能会受到较大的影响;

文件系统相关

  • 真题:

文件系统的实现需要考虑元数据(metadata)的处理,请回答跟元数据相关的下列问题:

(1)文件系统中的inode的作用是什么?inode中一般包含哪些信息?(4分)

(2)为了描述文件的大小,需要在元数据中给定指向数据的指针,指针一般分为直接指针(direct pointer)和间接指针(indirect pointer),假设数据块的大小为4KB,每个指针占用4字节的空间,那么10个直接指针和1个一级间接指针可以寻址的文件大小是多少?1个二级间接指针可以寻址的文件大小是多少?请给出分析和计算过程。(8分)

image-20220615145705046

  • 直接指针间接指针的好坏(来不及了…略)

    • 主要就是直接指针数目多,索引快,但是指向的数据块小
    • 间接指针需要额外的索引时间,但是数目少,指向的块的总大小很大

    image-20220615175410707

日志

日志记一下这五点吧,估计最多考这样

image-20220615175448501

CSAPP-LAB4-SHELL-LAB-REPORT

计科2002班

202001130329

杨铭

实验目的

通过这个实验能够更加深刻理解进程控制和信号控制的相关概念。本次实验我们要写一个自己的类linux/unix的shell程序,它能够支持工作的控制。

实验平台准备

  • Ubuntu-32 VMfare虚拟机
  • typora编写实验报告(经授课老师许可)

Hand Out 介绍

首先解压实验文件,然后做如下工作:

  • 使用命令tar xvf shlab-handout.tar解压文件
  • 使用命令make来编译和链接测试程序
  • 输入(你的队员)没有队员,把你的大名写在tsh.c程序的顶部注释中

注意到你的tsh.c(tiny shell)文件,你会看到它包含了一个简单Unix的shell的函数框架。为了简化实验,实际上题目已经帮我们实现了一些没那么有趣的函数。我们的任务就是完成剩下的空的函数。

需要实现

目标函数 说明
eval 主例程,用以分析和解释命令行(原型在课本8.4节)
builtin_cmd 执行bg和fg内置命令
waitfg 等待前台作业执行
sigchld_handler 响应处理SIGCHILD信号
sigint_handler 响应处理SIGINT(ctrl-c)信号
sigtstp_handler 响应处理SIGTP(ctrl-z)信号

每一次修改tsh.c,都需要输入make来重编译它。运行你的shell,在命令行输入tsh如下

1
2
unix> ./tsh
tsh> [type commands to your shell here]

普通的Unix Shells概述

一个shell是一个对于用户层面的交互程序,它能够对用户输入的命令行进行解析。一个shell通常会重复输出一个标识符(通常是’prompt‘),然后等待一个命令行输入或者stdin标准输入。一旦它取得了命令行之后就会用它的内容进行一系列的解析工作。

命令行是一个有序的ASCII字符序列,它们使用空格相隔。命令行的第一个单词通常是一个shell的内部命令或者一个可执行文件的路径名称。剩下的单词是命令行的参数。如果第一个单词是一个命令行的内部命令,shell会立即在当前进程(也就是shell)立即执行。否则的话,它是一个可执行文件的路径。在这个情况之下shell通过fork一个子进程,然后在子进程的上下文中加载并运行程序。由于解析一个命令行而创建的子进程又被称作是job(作业)。总的来说,一个job是由若干子进程组成的,这些子进程通过Unix管道进行连接。

如果一个命令行以“&”符号结尾,那么这个job就会在background运行该程序。也就是在后台运行这个程序,这意味着shell不会等待这个子进程结束后才打印下一个prompt来解析下一条命令行。因此在任何时候,最多只有一个任务能够在前台运行。然而在后台可以运行任意数目的作业。

例如,输入命令行

1
tsh> jobs

会导致shell执行一个内置的作业命令。输入命令如下

1
tsh> /bin/ls -l -d

来在前台运行一个ls程序。按照惯例,shell确保当程序开始执行的时候,它的主例程

1
int main(int argc, char *argv[])

这里的argcargv拥有了如下值:

  • argc == 3,
  • argv[0] == “/bin/ls”,
  • argv[1] == “-l”,
  • argv[2] == “-d”.

或者另一方面,我们输入命令行如下

1
tsh> /bin/ls -l -d &

就会使得ls程序在后台运行。

Unix shells支持job control,它能够允许用户对将作业在前台和后台之间移动,同时也能够改变进程的状态(running, stopped, or terminated)。输入ctrl-c会导致产生一个SIGINT信号,它被发送到前台的每个作业。类似的,如果你输入一个ctrl-z会导致一个SIGTSTP信号产生,并被发送给每一个前台的工作。对于SIGTSTP的默认行为是设置一个进程为stopped状态,它会保持停止状态直到直到它收到SIGCONT信号后才被唤醒。Unix shells同样提供不同的内置命令来支持这种工作的控制。例如

  • jobs:列出正在运行的以及停止的的后台作业
  • bg <job>:改变一个停止的后台作业为一个后台运行态作业
  • fg <job>:改变一个停止或运行的后台作业为一个运行的前台作业
  • kill <job>:终止一个作业

tsh的特点

我们的tsh应该具备以下特点

  • 标志符prompt应该是字符串“tsh>”
  • 用户输入的字符串应该由一个可执行的程序或者内嵌的指令开头(name),然后紧跟着0个或者更若干个参数。它们由一个或者更更多的空格隔开。如果name是一个内置指令,tsh直接处理执行,然后等待下一条指令。否则tsh就会把这个name当成是一个可执行文件的路径,然后初始化一个子进程,然后在这个子进程的上下文中加载执行这个程序(在这种情况下,我们所说的作业通常指这个子进程)
  • tsh不需要支持管道符(|)或者I/O的重定向(<和>)
  • 输入ctrl-c(ctrl-z)应该导致SIGINT(SIGTSTP)信号产生然后被发送到当前的前台作业那里去,这些信号同样会被发送到这些前台进程的后代进程那里去。(例如它所派生的子进程)。如果没有任何前台进程,那么这个信号将不会有任何的效果
  • 如果这个命令行是以&结尾的,那么tsh应该让这个作业在后台运行。
  • 每一个作业能够唯一的ID(PID)标识或者我们说这是一个作业ID(JID),它是一个tsh所分配的标识符。JIDs应该在命令行上用一个前缀’%‘来表示。例如:“%5”表示的是JID为5。(实验中提供了所有作业列表所需要的例程)
  • tsh应该支持以下内置命令
    • quit命令用来终止shell
    • jobs用来列出所有后台作业
    • bg <job>通过向<job>发送SIGCONT来重启它,然后让它运行在后台中,<job>参数可以用PID或者JID
    • fg <job>命令通过发送SIGCONT信号给进程<job>,然后把它运行在前台中,<job>参数可以用PID或者JID
  • tsh应该回收它的所有僵尸孩子。如果任何作业因为接收到未捕获的信号而终止,则tsh应识别此事件并打印带有作业 PID 的消息和违规信号的描述

检查工作的正确性

实验提供了一些工具来帮助我们检查工作是否正确。

Reference solution.tshref是一个Linux下的可执行文件为我们的shell提供了一个参考解决办法。运行这个程序来解决问题,看看我们的tsh执行的怎么样。你的shell应该提交输出和参考解决办法完全相同。

Shell driver.sdriver.pl程序将我们的tsh作为子进程执行,按照跟踪文件的指令向其发送命令和信号,并捕获显示shell的输出。使用-h

我们有16个trace文件trace{01-16}.txt,用来和shell driver一起使用,用来测试我们的shell的正确性。那些数字小的trace文件做一些简单的测试,大的数字做的是更加复杂的测试。

可以简单通过

1
2
unix> make test01		#执行test01
unix> make rtest01 #用reference solution(tshref)执行test01

看输出结果是否一致即可。

实验步骤

为了保证实验的顺利进行,首先阅读课本第八章的内容。

根据实验指导书,这边建议根据trace文件来进行实验,trace给出你要做的事情的提示。

main函数

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/*
* main - The shell's main routine
*/
int main(int argc, char **argv)
{
char c;
char cmdline[MAXLINE];
int emit_prompt = 1; /* emit prompt (default) */

/* Redirect stderr to stdout (so that driver will get all output
* on the pipe connected to stdout) */
dup2(1, 2); // 把错误信息重定向到标准输出上,也就是输出到屏幕上

/* Parse the command line */
// 处理参数
/*
* -h 帮助文件
* -v 发送额外的诊断信息
* -p 不打印prompt
*/
while ((c = getopt(argc, argv, "hvp")) != EOF) {
switch (c) {
case 'h': /* print help message */
usage();
break;
case 'v': /* emit additional diagnostic info */
verbose = 1;
break;
case 'p': /* don't print a prompt */
emit_prompt = 0; /* handy for automatic testing */
break;
default:
usage();
}
}

/* Install the signal handlers */
// 对各种信号进行处理
/* These are the ones you will need to implement */
Signal(SIGINT, sigint_handler); /* ctrl-c */
Signal(SIGTSTP, sigtstp_handler); /* ctrl-z */
Signal(SIGCHLD, sigchld_handler); /* Terminated or stopped child */

/* This one provides a clean way to kill the shell */
Signal(SIGQUIT, sigquit_handler);

/* Initialize the job list */
// 初始化作业列表
initjobs(jobs);

/* Execute the shell's read/eval loop */
while (1) {

/* Read command line */
// 打印一个prompt符
if (emit_prompt) {
printf("%s", prompt);
fflush(stdout);
}
// 从标准获得命令行
if ((fgets(cmdline, MAXLINE, stdin) == NULL) && ferror(stdin))
app_error("fgets error");

// EOF: 当输入ctrl-d的时候表示标准输入(文件)结束,此时直接退出
if (feof(stdin)) { /* End of file (ctrl-d) */
fflush(stdout);
exit(0);
}

/* Evaluate the command line */
eval(cmdline); // 解析命令行
fflush(stdout);
fflush(stdout);
}

exit(0); /* control never reaches here */
}

从上面的代码我们可以看出,main函数的主要工作就是从标准输出中读出命令行,然后把它交给eval来处理。

参考CSAPP官方网站上给出阉割版本的shell.c,我们可以看到eval主要工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 
* eval - evaluate a command line
*/
void eval(char *cmdline)
{
...
// 通过把argv传递给buildin_command让它判断是不是内置命令并执行
// 如果不是返回零
if (!builtin_command(argv)) {
...
}

/*
* buildin_cmd
*/
int builtin_cmd(char **argv)
{
...

我们理清楚主要执行流程如下

image-20220525154122346

test01

1
2
3
4
5
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest01
./sdriver.pl -t trace01.txt -s ./tshref -a "-p"
#
# trace01.txt - Properly terminate on EOF.
#

我们参考第一个test,对tshref输出了如上结果。这个对于标准输入结束(EOF)的处理。实际上就是对ctrl-d的处理。我们观察到上面main函数中已经有包含对EOF的处理

1
2
3
4
5
// EOF: 当输入ctrl-d的时候表示标准输入(文件)结束,此时直接退出
if (feof(stdin)) { /* End of file (ctrl-d) */
fflush(stdout);
exit(0);
}

因此我们不许要做什么,可以看到测试结果和参考一致。

1
2
3
4
5
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make test01
./sdriver.pl -t trace01.txt -s ./tsh -a "-p"
#
# trace01.txt - Properly terminate on EOF.
#

test02

这个一个明显需要做些什么。(不可能又帮你写好)

1
2
3
4
5
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest02
./sdriver.pl -t trace02.txt -s ./tshref -a "-p"
#
# trace02.txt - Process builtin quit command.
#

这个测试要求我们完成对tsh的一条内嵌指令(quit)的处理。它完成的任务就是退出tsh

1
2
3
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ ./tsh
tsh> quit
tsh>

我们可以看到如果什么都不做,是不能够退出的。

现在对这个quit指令的位置有两种思考方向。根据上面main的执行流程,我们说有两个后续处理的方向,一个就是放在eval里面处理,一个就是放在buildin_cmd里面。后者很好理解,这个quit本来就是内嵌指令,所以可以放在后者里面。

1
int builtin_cmd(char **argv);

可以看到buildin_cmd虽然是对内置指令的处理,这样一条quit非常简单,只要读到这条指令,直接exit(0),就是它的全部逻辑了。

首先用到parseline来解析命令行,并建立argv,我们可以看到parseline函数的说明如下:

1
2
3
4
5
6
7
/* 
* parseline - Parse the command line and build the argv array.
*
* Characters enclosed in single quotes are treated as a single
* argument. Return true if the user has requested a BG job, false if
* the user has requested a FG job.
*/

这个函数主要主要功能就是解析命令行字符串,然后建立argv参数数组,用来给后面execve使用等。

所以代码实现起来就是如下

1
2
3
4
5
6
7
8
9
10
11
12
void eval(char *cmdline) 
{
// 参数字符串数组
char *argv[MAXARGS]; /* argv for execve() */
int bg; /* should the job run in bg or fg? */

/* parse command line */
// 解析cmdline获得argv
bg = parseline(cmdline, argv);
if (!builtin_cmd(argv)) {
...
}
1
2
3
4
5
6
7
8
9
/* 
* builtin_cmd - If the user has typed a built-in command then execute
* it immediately.
*/
int builtin_cmd(char **argv)
{
if (!strcmp(argv[0], "quit"))
exit(0); /* terminate shell */
...

然后我们make编译以下,查看测试情况,

1
2
3
4
5
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make test02
./sdriver.pl -t trace02.txt -s ./tsh -a "-p"
#
# trace02.txt - Process builtin quit command.
#

和参考一样,通关~

test03 & test04

测试3和测试4的内容放在一起讲好了,因为

1
2
3
4
5
6
7
8
9
10
11
12
13
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest03
./sdriver.pl -t trace03.txt -s ./tshref -a "-p"
#
# trace03.txt - Run a foreground job.
#
tsh> quit
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest04
./sdriver.pl -t trace04.txt -s ./tshref -a "-p"
#
# trace04.txt - Run a background job.
#
tsh> ./myspin 1 &
[1] (9950) ./myspin 1 &

可以看到参考程序中,对于测试3,它测试了前台运行quit,测试4在后台运行了myspinmyspin是自旋等待了1秒。

1
2
3
4
5
6
// in `parseline`

/* should the job run in the background? */
if ((bg = (*argv[argc-1] == '&')) != 0) {
argv[--argc] = NULL;
}

可以看到在parseline通过上面的步骤解析判断了是否需要讲进程运行在后台。主要是通过检索命令行末尾的字符是否是‘&’

前台工作需要等待工作执行完毕就好了。不论是前台还是后台程序,都需要加入到jobs

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
/* global variables */
int nextjid = 1; /* next job ID to allocate */
...
/* addjob - Add a job to the job list */
int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline)
{
int i;
if (pid < 1) // 无效pid
return 0;

for (i = 0; i < MAXJOBS; i++) {
/* 如果jobs数组里面有pid等于0等于0的表项,表示这个位置可以用,把这个作业放进去 */
if (jobs[i].pid == 0) {
jobs[i].pid = pid;
jobs[i].state = state;
jobs[i].jid = nextjid++;
if (nextjid > MAXJOBS)
nextjid = 1;
strcpy(jobs[i].cmdline, cmdline);
if(verbose){
printf("Added job [%d] %d %s\n", jobs[i].jid, jobs[i].pid, jobs[i].cmdline);
}
return 1;
}
}
printf("Tried to create too many jobs\n");
return 0;
}

addjob的工作就是为PID为pid的子进程分配一个jid,然后把jid保存在进程结构中,然后根据全局变量JID来给这个子进程为tsh编写这个子进程的JID。通过是为了防止JID越界的情况,需要额外对的nextjid进行判断。我们对此更加加深了对jid的理解。jid就是现在一共有几个job在tsh下面运行。由于jobs数组是有限的,所以jid超过了MAXJOBS的话是需要重置的。如果发生这种事情的话,只有当一个作业结束,jobs才能让出位置来,这是后才可以再分配工作。值得一提的是,根据课本shell.c的代码,我们可以在这里加一个,如果verbose有效的话,打印详细信息(加入的作业是啥)

我们还需要等待前台运行的子进程运行完,所以我们还要解决waitfg函数的问题。这个函数是需要我们自己编写的。主要功能就是阻塞当前进程也就是tsh,直到我们pid命名的子进程结束。

1
2
3
4
5
6
7
8
9
/* fgpid - Return PID of current foreground job, 0 if no such job */
pid_t fgpid(struct job_t *jobs) {
int i;

for (i = 0; i < MAXJOBS; i++)
if (jobs[i].state == FG)
return jobs[i].pid;
return 0;
}

上面这个函数就是循环查看当前前台工作的作业的pid是多少。知道了这个我们就可以写waitfg函数了。当pid对应的进程还是前台进程的时候就一直循环等待。指导书里面说可以用循环sleep(0)进行等待。

1
2
3
4
5
void waitfg(pid_t pid)
{
while (pid == fgpid(jobs))
sleep(0); // 这里是指主动让出CPU的意思
}

具备上面的基础,我们可以写出代码如下:

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
// in `eval`
...

if (!builtin_cmd(argv)) {
if ((pid = fork()) == 0) { /* child */

/* Background jobs should ignore SIGINT (ctrl-c) */
/* and SIGTSTP (ctrl-z) */
/*
if (bg) {
// 课本对SIG_IGN有详细解释
// 如果信号的handler是SIG_IGN,会忽略这个信号
Signal(SIGINT, SIG_IGN);
Signal(SIGTSTP, SIG_IGN);
}
*/

// 前台执行
if (execve(argv[0], argv, environ) < 0) {
printf("%s: Command not found.\n", argv[0]);
fflush(stdout);
exit(0); // 如果没有这个可执行程序,那么直接终止这个子进程
}
}
/* parent waits for foreground job to terminate or stop */
addjob(jobs, pid, (bg == 1 ? BG : FG), cmdline);
if (!bg) // 前台运行则等待子进程
waitfg(pid);
else
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
}

可以看到上面的函数fork出一个子进程,然后execve执行某个可执行程序。对于父进程,父进程把这个进程添加到jobs队列中去。然后如果这是一个前台程序,就调用waitfg等待子进程结束。如果子进程结束了。通过SIGCHLD信号给父进程发信息,唤醒父进程回收自己。

父进程回收处理程序如下:

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
/* 
* sigchld_handler - The kernel sends a SIGCHLD to the shell whenever
* a child job terminates (becomes a zombie), or stops because it
* received a SIGSTOP or SIGTSTP signal. The handler reaps all
* available zombie children, but doesn't wait for any other
* currently running children to terminate.
*/
void sigchld_handler(int sig)
{
pid_t pid;
int status;

if (verbose)
printf("sigchld_handler: entering \n");

/*
* 回收僵尸进程
* 这里的WNOHANG是非常重要的。
* 它的本意是如果所有孩子都没有僵尸(终止)状态的,直接退出
* 这个能够避免在这里等待所有前台的running和stopped程序终止
* 这样tsh就不能正常接受用户的输入了
* WUNTRACED是等待直到有一个子进程变成僵尸退出,返回它的pid
* 这个选项开启能够检查已终止和被停止的子进程
*/
while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
// 这里不管是不是正常exit或者ctrl-c或者ctrl-z退出的
// 子进程都结束了,都要回收资源
deletejob(jobs, pid);
if (verbose)
printf("sigchld_handler: job %d deleted\n", pid);
}
if (verbose)
printf("sigchld_handler: exiting\n");
}

像上面这样写看起来是没有什么问题了。但是课本519中提示了一种苛刻同步问题。如果子进程在父进程将自己加入到jobs之前就执行完毕,然后exit退出,就会触发父进程的回收sigchld_handler程序,实际上,这样的删除显然没有任何意义。后续又将这个工作addjob,这也是没有什么意义的。

因此我们还要对上面的eval进一步修改为:

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
// in `eval`
...
sigset_t mask;

if (!builtin_cmd(argv)) {
Sigprocmask(SIG_BLOCK, &mask, &prev); /* 阻塞SIGCHLD */

if ((pid = fork()) == 0) { /* Child runs user job */
/* 给fork出来的子进程设置一个独立的组,
* 子进程是这个组的组长,组id为子进程的pid */
setpgid(0, 0);

/* 对于子进程并不需要阻塞SIGCHLD的信号 */
Sigprocmask(SIG_UNBLOCK, &prev, NULL); // unblock SIGCHLD

/*
* 前台执行
* 如果没有这个可执行程序,那么直接终止这个子进程
*/
if (execve(argv[0], argv, environ) < 0) {
printf("%s: Command not found.\n", argv[0]);
_exit(1);
}
}

addjob(jobs, pid, bg ? BG : FG, cmdline);
/* 已经加到jobs了,解除阻塞 */
Sigprocmask(SIG_SETMASK, &prev, NULL);


/* Parent waits for foreground job to terminate */
if (!bg) // 前台运行则等待子进程
waitfg(pid);
else // background
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
}

测试以下我们的代码的正确性

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
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest03
./sdriver.pl -t trace03.txt -s ./tshref -a "-p"
#
# trace03.txt - Run a foreground job.
#
tsh> quit
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make test03
./sdriver.pl -t trace03.txt -s ./tsh -a "-p"
#
# trace03.txt - Run a foreground job.
#
tsh> quit


puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest04
./sdriver.pl -t trace04.txt -s ./tshref -a "-p"
#
# trace04.txt - Run a background job.
#
tsh> ./myspin 1 &
[1] (10488) ./myspin 1 &
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make test04
./sdriver.pl -t trace04.txt -s ./tsh -a "-p"
#
# trace04.txt - Run a background job.
#
tsh> ./myspin 1 &
[1] (10494) ./myspin 1 &

测试程序都相同了,成功~

test05

1
2
3
4
5
6
7
8
9
10
11
12
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest05
./sdriver.pl -t trace05.txt -s ./tshref -a "-p"
#
# trace05.txt - Process jobs builtin command.
#
tsh> ./myspin 2 &
[1] (10504) ./myspin 2 &
tsh> ./myspin 3 &
[2] (10506) ./myspin 3 &
tsh> jobs
[1] (10504) Running ./myspin 2 &
[2] (10506) Running ./myspin 3 &

这里的任务是完成内嵌指令jobs的工作。它能够列出作业列表中的所有作业。

这个很简单,起始只要用一个listjobs就可已完成这个工作了,这个函数是实验已经帮我们实现了的。

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
/* listjobs - Print the job list */
void listjobs(struct job_t *jobs)
{
int i;

for (i = 0; i < MAXJOBS; i++) {
if (jobs[i].pid != 0) {
printf("[%d] (%d) ", jobs[i].jid, jobs[i].pid);
switch (jobs[i].state) {
case BG:
printf("Running ");
break;
case FG:
printf("Foreground ");
break;
case ST:
printf("Stopped ");
break;
default:
printf("listjobs: Internal error: job[%d].state=%d ",
i, jobs[i].state);
}
printf("%s", jobs[i].cmdline);
}
}
}

然后在builtin_cmd中加入jobs命令的入口就好。

1
2
3
4
5
6
7
8
9
int builtin_cmd(char **argv) 
{
...
// jobs command
if (!strcmp(argv[0], "jobs")) {
listjobs(jobs);
return 1;
}
...

我们看看测试结果

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
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest05
./sdriver.pl -t trace05.txt -s ./tshref -a "-p"
#
# trace05.txt - Process jobs builtin command.
#
tsh> ./myspin 2 &
[1] (10613) ./myspin 2 &
tsh> ./myspin 3 &
[2] (10615) ./myspin 3 &
tsh> jobs
[1] (10613) Running ./myspin 2 &
[2] (10615) Running ./myspin 3 &

puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make test05
./sdriver.pl -t trace05.txt -s ./tsh -a "-p"
#
# trace05.txt - Process jobs builtin command.
#
tsh> ./myspin 2 &
[1] (10622) ./myspin 2 &
tsh> ./myspin 3 &
[2] (10624) ./myspin 3 &
tsh> jobs
[1] (10622) Running ./myspin 2 &
[2] (10624) Running ./myspin 3 &

和参考一致,表示正确。这里你可能会被英文的实验指导书迷惑了,实际上列出的是工作列表的所有工作,而不是后台的所有工作。通关~

test06 & test07 & test08

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
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest06
./sdriver.pl -t trace06.txt -s ./tshref -a "-p"
#
# trace06.txt - Forward SIGINT to foreground job.
#
tsh> ./myspin 4
Job [1] (10840) terminated by signal 2
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest07
./sdriver.pl -t trace07.txt -s ./tshref -a "-p"
#
# trace07.txt - Forward SIGINT only to foreground job.
#
tsh> ./myspin 4 &
[1] (10846) ./myspin 4 &
tsh> ./myspin 5
Job [2] (10848) terminated by signal 2
tsh> jobs
[1] (10846) Running ./myspin 4 &
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest08
./sdriver.pl -t trace08.txt -s ./tshref -a "-p"
#
# trace08.txt - Forward SIGTSTP only to foreground job.
#
tsh> ./myspin 4 &
[1] (10856) ./myspin 4 &
tsh> ./myspin 5
Job [2] (10858) stopped by signal 20
tsh> jobs
[1] (10856) Running ./myspin 4 &
[2] (10858) Stopped ./myspin 5

测试678要完成的任务是对于SIGINT和SIGTSTP的处理。对于上面的测试,测试6启动一个自旋程序到前台并且发出SIGINT终止它。测试7启动两个自旋,一个在前台一个在后台,然后发出SIGINT,检测到只有前台进程被终止了。测试8类似。

image-20220526003548830

我们可以从课本上这幅图看到shell会给每一个子进程分配一个和PID一样的组id,这些shell的子进程的孩子都在这些子进程的组内。比如上图中的前台作业就是shell产生的子进程,但是它和shell不是一个组的,他自己独立成组,组id等于自己的pid,然后它的孩子都属于自己这个组。

首先需要完成SIGINT的处理,给前台进程的组的所有进程发送SIGINT信号(这里就是参数sig)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void sigint_handler(int sig)
{
// 这一步的保存和下一步的恢复,是由于在下面的kill系统调用中可能会覆盖errno的值
// 导致现在的errno丢失
// 而errno在sigchild_handler中还有使用到,为了避免产生这种错误需要在这里保存,在后面恢复
int olderrno = errno;
// get the foreground job pid
pid_t fg_pid = fgpid(jobs);
/*
* int kill(pid_t pid,int signo)
* 功能: 向进程或进程组发送一个信号 (成功返回 0; 否则,返回 -1 )
*/
kill(-fg_pid, sig);
// -fg_pid表示向进程组号为pid的组中的每个进程发sig信号
// 在这里就是向前台进程以及它的每一个子进程(子进程都在自己的父进程的pid为组id的组下)
errno = olderrno;
}

相同的原理,可以写出sigtstp_handler函数

1
2
3
4
5
6
7
8
9
10
void sigtstp_handler(int sig)
{
// 为了异步信号安全防止errno被覆盖
int olderrno = errno;
// get the foreground job pid
pid_t fg_pid = fgpid(jobs);
// kill the group in the foreground
kill(-fg_pid, sig);
errno = olderrno;
}

上面的两个函数就是信号处理过程。sigchld_handler里收到子进程终止或停止的消息后给出对应的输出然后改变其状态,对于终止的进程就在jobs里将其删除,对于停止的进程则设置其state为ST。

image-20220526081536379

主要流程如上图。

然后就是对sigchld_handler的修改。我们需要针对上面的代码进一步进行修改。

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
42
43
44
45
46
47
48
/*
* sigchld_handler - The kernel sends a SIGCHLD to the shell whenever
* a child job terminates (becomes a zombie), or stops because it
* received a SIGSTOP or SIGTSTP signal. The handler reaps all
* available zombie children, but doesn't wait for any other
* currently running children to terminate.
*/
void sigchld_handler(int sig)
{
int old_errno = errno;

pid_t pid;
int status;

if (verbose)
printf("sigchld_handler: entering \n");

/*
* 回收僵尸进程
* 这里的WNOHANG是非常重要的。
* 它的本意是如果所有孩子都没有僵尸(终止)状态的,直接退出
* 这个能够避免在这里等待所有前台的running和stopped程序终止
* 这样tsh就不能正常接受用户的输入了
* WUNTRACED是等待直到有一个子进程变成僵尸退出,返回它的pid
*/
while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
/* 这里实验指导书说不能用while,但是不用会出现错误。他说的while和我的判断条件不一样 */
// 子进程正常退出
if (WIFEXITED(status)) {
deletejob(jobs, pid);
}
// 子进程因为ctrl-c退出
if (WIFSIGNALED(status)) { // terminated by ctrl-c
printf("Job [%d] (%d) terminated by signal %2d\n",
pid2jid(pid), pid, WTERMSIG(status));
deletejob(jobs, pid);
}
if (WIFSTOPPED(status)) { // stopped by ctrl-z
printf("Job [%d] (%d) stopped by signal %2d\n",
pid2jid(pid), pid, WSTOPSIG(status));
// 修改子进程状态为ST
getjobpid(jobs, pid)->state = ST;
}
}
errno = old_errno;
if (verbose)
printf("sigchld_handler: exiting\n");
}

上面的程序,在子进程发送SIGCHLD时处于不同状态的子进程,已经有了不同的处理。

但是还存在一个问题,就是printf是异步不安全的。可能出现死锁现象。

死锁(Deadlock):当你存在多个逻辑流在等待永远不会发生的场景时就会出现死锁。比如在信号处理程序中使用异步信号不安全的printf函数就可能会出现死锁现象。在主程序中执行了printf函数,则该函数会请求某些资源的一个锁,当该printf函数请求这个锁时它被某个信号处理程序中断了,而在信号处理程序中也要执行一个printf函数,这个printf也试图请求那个锁,但是由于主程序中的printf函数持有那个锁,所以信号处理程序中的printf得不到那个锁,所以这个printf就在等待那个锁被释放的锁,但是主程序只有在信号处理程序返回时才可能释放那个锁,所以这里就造成了死锁。

这里是线程安全但是不可重入的异步安全问题,使用锁是不能够解决的。

所以我们再主线程也就是main里面已经使用过printf了,就要避免在信号处理程序中在使用printf的出现。

我提供下面两种解决手段。

  • 一种想法是在这里使用printf的时候屏蔽或者说阻塞SIGCHLD信号,这样主线程在使用printf就不会被中断。但是带来的问题就是这个中断处理信号会被忽略。可能出现处理遗漏的情况。
  • 还有一种处理手段是使用异步信号安全的可重入函数,如下

CSAPP课本实例程序提供了一种线程安全且可重入的函数sio系列函数,使用它,不调用printf

1
2
3
4
5
6
7
8
/* private functions */
static void sio_reverse(char s[]);
static void sio_ltoa(long v, char s[], int b);
static size_t sio_strlen(char s[]);
/* public functions */
int sio_puts(char s[]);
int sio_putl(long v);
void sio_error(char s[]);

查看Linux手册可以知道write是异步信号安全的,使用write就可以实现异步安全的输出了。

1
2
3
4
int sio_puts(char s[]) /* Put string */
{
return write(STDOUT_FILENO, s, strlen(s));
}

我对上面的sio族进行封装形成Sio系列。

我们把sigchld_handler修改以下如下

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/*
* sigchld_handler - The kernel sends a SIGCHLD to the shell whenever
* a child job terminates (becomes a zombie), or stops because it
* received a SIGSTOP or SIGTSTP signal. The handler reaps all
* available zombie children, but doesn't wait for any other
* currently running children to terminate.
*/
void sigchld_handler(int sig)
{
int old_errno = errno;

pid_t pid;
int status;

if (verbose)
printf("sigchld_handler: entering \n");

/*
* 回收僵尸进程
* 这里的WNOHANG是非常重要的。
* 它的本意是如果所有孩子都没有僵尸(终止)状态的,直接退出
* 这个能够避免在这里等待所有前台的running和stopped程序终止
* 这样tsh就不能正常接受用户的输入了
* WUNTRACED是等待直到有一个子进程变成僵尸退出,返回它的pid
*/
while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
// 子进程正常退出
if (WIFEXITED(status)) {
deletejob(jobs, pid);
}
// 子进程因为ctrl-c退出
if (WIFSIGNALED(status)) { // terminated by ctrl-c
/* printf("Job [%d] (%d) terminated by signal %2d\n",
pid2jid(pid), pid, WTERMSIG(status)); */ {
Sio_puts("Job [");
Sio_putl(pid2jid(pid));
Sio_puts("] (");
Sio_putl(pid);
Sio_puts(") terminated by signal ");
Sio_putl(WTERMSIG(status));
Sio_puts("\n");
}
deletejob(jobs, pid);
}
if (WIFSTOPPED(status)) { // stopped by ctrl-z
/* printf("Job [%d] (%d) stopped by signal %2d\n",
pid2jid(pid), pid, WSTOPSIG(status)); */ {
Sio_puts("Job [");
Sio_putl(pid2jid(pid));
Sio_puts("] (");
Sio_putl(pid);
Sio_puts(") stopped by signal ");
Sio_putl(WSTOPSIG(status));
Sio_puts("\n");
}
// 修改子进程状态为ST
getjobpid(jobs, pid)->state = ST;
}
}
errno = old_errno;
if (verbose) {
/* printf("sigchld_handler: exiting\n"); */ {
Sio_puts("sigchld_handler: exiting\n");
}
}
}

这样就不会在处理信号程序中使用异步信号不安全的printf了。

然后,shell程序本身是所有子进程的父进程,按照fork函数简单实现,会被分配在一个组中。如果收到了SIGINT,发送给子进程的组,那也会发送给自己(子进程就在自己的组中),那么自己又会收到SIGINT,然后又会发送给自己的所有子进程,然后自己又收到…就陷入循环中无法继续,或者崩溃退出。所以shell不可以和它的子进程在同一个组内,就是要给每个子进程单独的组,组的id就是子进程的pid。我们应该把这个操作放在fork之后,修改它的组id,然后执行execve

修改eval这一部分代码如下

1
2
3
4
5
if ((pid = fork()) == 0) {  /* child */
/* 给fork出来的子进程设置一个独立的组,
* 子进程是这个组的组长,组id为子进程的pid */
setpgid(0, 0);
...

setpgid函数在实验英文指导书有介绍,用于设置进程组id

表头文件 #include

定义函数 int setpgid(pid_t pid,pid_t pgid);

功能:设置pid所在的进程组的进程组id设置成pgid

setpgid(0, 0)的含义:

  • 第一个参数pid是0,则使用当前进程的pid
  • 第二个参数pgid是0,则使用当前进程pid作为pgid

测试结果

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest06
./sdriver.pl -t trace06.txt -s ./tshref -a "-p"
#
# trace06.txt - Forward SIGINT to foreground job.
#
tsh> ./myspin 4
Job [1] (3427) terminated by signal 2
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make test06
./sdriver.pl -t trace06.txt -s ./tsh -a "-p"
#
# trace06.txt - Forward SIGINT to foreground job.
#
tsh> ./myspin 4
Job [1] (3433) terminated by signal 2


puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest07
./sdriver.pl -t trace07.txt -s ./tshref -a "-p"
#
# trace07.txt - Forward SIGINT only to foreground job.
#
tsh> ./myspin 4 &
[1] (3440) ./myspin 4 &
tsh> ./myspin 5
Job [2] (3442) terminated by signal 2
tsh> jobs
[1] (3440) Running ./myspin 4 &
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make test07
./sdriver.pl -t trace07.txt -s ./tsh -a "-p"
#
# trace07.txt - Forward SIGINT only to foreground job.
#
tsh> ./myspin 4 &
[1] (3449) ./myspin 4 &
tsh> ./myspin 5
Job [2] (3451) terminated by signal 2
tsh> jobs
[1] (3449) Running ./myspin 4 &


puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest08
./sdriver.pl -t trace08.txt -s ./tshref -a "-p"
#
# trace08.txt - Forward SIGTSTP only to foreground job.
#
tsh> ./myspin 4 &
[1] (3459) ./myspin 4 &
tsh> ./myspin 5
Job [2] (3461) stopped by signal 20
tsh> jobs
[1] (3459) Running ./myspin 4 &
[2] (3461) Stopped ./myspin 5
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make test08
./sdriver.pl -t trace08.txt -s ./tsh -a "-p"
#
# trace08.txt - Forward SIGTSTP only to foreground job.
#
tsh> ./myspin 4 &
[1] (3469) ./myspin 4 &
tsh> ./myspin 5
Job [2] (3471) terminated by signal 20
tsh> jobs
[1] (3469) Running ./myspin 4 &
[2] (3471) Stopped ./myspin 5

可以看到与参考输出一致,通关~

test09 & test10

第九个和第十个放在一起写。请往下看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest09
./sdriver.pl -t trace09.txt -s ./tshref -a "-p"
#
# trace09.txt - Process bg builtin command
#
tsh> ./myspin 4 &
[1] (3480) ./myspin 4 &
tsh> ./myspin 5
Job [2] (3482) stopped by signal 20
tsh> jobs
[1] (3480) Running ./myspin 4 &
[2] (3482) Stopped ./myspin 5
tsh> bg %2
[2] (3482) ./myspin 5
tsh> jobs
[1] (3480) Running ./myspin 4 &
[2] (3482) Running ./myspin 5

我们先看看第九个测试的参考输出。我们可以看到我们需要构建的是bg内置命令的相关功能。

bg <job>通过向<job>发送SIGCONT来重启它,然后让它运行在后台中,<job>参数可以用PID或者JID

  • 如果参数是PID则直接输入pid即可
  • 如果参数是JID则输入“%jid”

我们看到上面的测试。首先是开启一个后台自旋和一个前台自旋的程序。然后发起ctrl-z终止前台的进程。然后jobs可以看到两个进程以及状态。后台的进程仍然在正常运行,但是前台的进程由于SIGTSTP信号而进入stopped状态。然后使用bg %2指令,把第二个子进程(停止的“./myspin 5”)唤醒,然后放到后台执行。输入jobs指令可以看到两个都在执行,但是后者是后来被切换到后台的,所以没有它的cmdline还没有更新还是显示原来的“./myspin 5”。

再看看rtest10,第十个测试

1
2
3
4
5
6
7
8
9
10
11
12
13
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest10
./sdriver.pl -t trace10.txt -s ./tshref -a "-p"
#
# trace10.txt - Process fg builtin command.
#
tsh> ./myspin 4 &
[1] (3510) ./myspin 4 &
tsh> fg %1
Job [1] (3510) stopped by signal 20
tsh> jobs
[1] (3510) Stopped ./myspin 4 &
tsh> fg %1
tsh> jobs

它针对的是fg这个内置命令的处理。

fg <job>命令通过发送SIGCONT信号给进程<job>,然后把它运行在前台中,<job>参数可以用PID或者JID

  • 如果参数是PID则直接输入pid即可
  • 如果参数是JID则输入“%jid”

可以看到上面的测试样例,一开始,是后台运行一个“./myspin 4”,然后把它放到前台运行,使用“fg %1”命令,切换到前台执行,shell等待它执行完毕。然后jobs查看看到有一个停止的工作,然后把它放到前台并开始运行。最后jobs查看,由于已经运行完了,所以没有输出。

对于这两个功能我们要实现的是do_fgbg函数

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
/*
* do_bgfg - Execute the builtin bg and fg commands
*/
void do_bgfg(char** argv)
{
struct job_t* job;
char* id = argv[1];

// 判断输入的是jid还是pid
if (id[0] == '%') { /* jid */
//去掉'%'开始读jid,根据jid返回这个子进程的结构指针
int jid = atoi(id + 1);
job = getjobjid(jobs, jid);
}
else { /* pid */
int pid = atoi(id);
job = getjobpid(jobs, pid);
}

/*
* kill不单是杀掉进程,还有发送信号的功能
* 这里唤醒job所在的组中的所有进程
* 就是唤醒这个stopped的子进程,以及它派生的孙子进程
*/
kill(-(job->pid), SIGCONT);

if (!strcmp(argv[0], "fg")) { // fg command
job->state = FG;
// 等待该前台作业终止
waitfg(job->pid);
}
else { // bg command
job->state = BG;
/* 切换到bg后打印作业信息 */
printf("[%d] (%d) %s", pid2jid(job->pid), job->pid, job->cmdline);
}
}

然后我们在builtin_cmd中加入函数入口。

1
2
3
4
5
6
7
// in `builtin_cmd`

// bg or fg command
if (!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) {
do_bgfg(argv);
return 1;
}

查看测试结果

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest09
./sdriver.pl -t trace09.txt -s ./tshref -a "-p"
#
# trace09.txt - Process bg builtin command
#
tsh> ./myspin 4 &
[1] (3627) ./myspin 4 &
tsh> ./myspin 5
Job [2] (3629) stopped by signal 20
tsh> jobs
[1] (3627) Running ./myspin 4 &
[2] (3629) Stopped ./myspin 5
tsh> bg %2
[2] (3629) ./myspin 5
tsh> jobs
[1] (3627) Running ./myspin 4 &
[2] (3629) Running ./myspin 5
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make test09
./sdriver.pl -t trace09.txt -s ./tsh -a "-p"
#
# trace09.txt - Process bg builtin command
#
tsh> ./myspin 4 &
[1] (3638) ./myspin 4 &
tsh> ./myspin 5
Job [2] (3640) terminated by signal 20
tsh> jobs
[1] (3638) Running ./myspin 4 &
[2] (3640) Stopped ./myspin 5
tsh> bg %2
[2] (3640) ./myspin 5
tsh> jobs
[1] (3638) Running ./myspin 4 &
[2] (3640) Running ./myspin 5


puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest10
./sdriver.pl -t trace10.txt -s ./tshref -a "-p"
#
# trace10.txt - Process fg builtin command.
#
tsh> ./myspin 4 &
[1] (7350) ./myspin 4 &
tsh> fg %1
Job [1] (7350) stopped by signal 20
tsh> jobs
[1] (7350) Stopped ./myspin 4 &
tsh> fg %1
tsh> jobs
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make test10
./sdriver.pl -t trace10.txt -s ./tsh -a "-p"
#
# trace10.txt - Process fg builtin command.
#
tsh> ./myspin 4 &
[1] (7361) ./myspin 4 &
tsh> fg %1
Job [1] (7361) stopped by signal 20
tsh> jobs
[1] (7361) Stopped ./myspin 4 &
tsh> fg %1
tsh> jobs

与参考一致,通关~

test11 & test12 & test13

先看看test11

1
2
3
4
5
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest11
./sdriver.pl -t trace11.txt -s ./tshref -a "-p"
#
# trace11.txt - Forward SIGINT to every process in foreground process group
#

要求我们接收到SIGINT的时候把它发送给前台进程所处的进程组的所有进程。这个在我们前面的代码中的sigint_handler就已经有所处理了。

然后看看test12

1
2
3
4
5
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest12
./sdriver.pl -t trace12.txt -s ./tshref -a "-p"
#
# trace12.txt - Forward SIGTSTP to every process in foreground process group
#

和测试11类似,我们接受到SIGTSTP的时候要把信号发给前台进程所处的进程组的所有进程。这个在sigtstp_handler就已经处理了。

再看看test13

1
2
3
4
5
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest13
./sdriver.pl -t trace13.txt -s ./tshref -a "-p"
#
# trace13.txt - Restart every stopped process in process group
#

这个测试检测的是能够重启一个进程(原来是stopped状态),这个功能在对于内置指令fg以及bg的处理上已经实现了。

对于Linux下的ps命令a参数表示显示所有的进程

状态码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Here are the different values that the s, stat and state output specifiers (header "STAT" or "S") will display to describe the state of a process:

D uninterruptible sleep (usually IO)
R running or runnable (on run queue)
S interruptible sleep (waiting for an event to complete)
T stopped by job control signal
t stopped by debugger during the tracing
W paging (not valid since the 2.6.xx kernel)
X dead (should never be seen)
Z defunct ("zombie") process, terminated but not reaped by its parent

For BSD formats and when the stat keyword is used, additional characters may be displayed:
< high-priority (not nice to other users)
N low-priority (nice to other users)
L has pages locked into memory (for real-time and custom IO)
s is a session leader
l is multi-threaded (using CLONE_THREAD, like NPTL pthreads do)
+ is in the foreground process group

我们直接对比测试结果

  • test11
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
42
43
44
45
46
47
48
49
50
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest11
./sdriver.pl -t trace11.txt -s ./tshref -a "-p"
#
# trace11.txt - Forward SIGINT to every process in foreground process group
#
tsh> ./mysplit 4
Job [1] (7419) terminated by signal 2
tsh> /bin/ps a
PID TTY STAT TIME COMMAND
1190 tty4 Ss+ 0:00 /sbin/getty -8 38400 tty4
1197 tty5 Ss+ 0:00 /sbin/getty -8 38400 tty5
1207 tty2 Ss+ 0:00 /sbin/getty -8 38400 tty2
1209 tty3 Ss+ 0:00 /sbin/getty -8 38400 tty3
1230 tty6 Ss+ 0:00 /sbin/getty -8 38400 tty6
1352 tty7 Ss+ 23:36 /usr/bin/X :0 -auth /var/run/lightdm/root/:0 -nolisten tcp vt7 -novtswitch -background none
1785 tty1 Ss+ 0:00 /sbin/getty -8 38400 tty1
6865 pts/0 Ss 0:00 bash
6971 pts/1 Ss+ 0:00 /bin/bash
7255 pts/0 R 39:18 ./tsh -p
7256 pts/0 Z 0:00 [echo] <defunct>
7414 pts/0 S+ 0:00 make rtest11
7415 pts/0 S+ 0:00 /bin/sh -c ./sdriver.pl -t trace11.txt -s ./tshref -a "-p"
7416 pts/0 S+ 0:00 /usr/bin/perl ./sdriver.pl -t trace11.txt -s ./tshref -a -p
7417 pts/0 S+ 0:00 ./tshref -p
7422 pts/0 R 0:00 /bin/ps a
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make test11
./sdriver.pl -t trace11.txt -s ./tsh -a "-p"
#
# trace11.txt - Forward SIGINT to every process in foreground process group
#
tsh> ./mysplit 4
Job [1] (7433) terminated by signal 2
tsh> /bin/ps a
PID TTY STAT TIME COMMAND
1190 tty4 Ss+ 0:00 /sbin/getty -8 38400 tty4
1197 tty5 Ss+ 0:00 /sbin/getty -8 38400 tty5
1207 tty2 Ss+ 0:00 /sbin/getty -8 38400 tty2
1209 tty3 Ss+ 0:00 /sbin/getty -8 38400 tty3
1230 tty6 Ss+ 0:00 /sbin/getty -8 38400 tty6
1352 tty7 Ss+ 23:37 /usr/bin/X :0 -auth /var/run/lightdm/root/:0 -nolisten tcp vt7 -novtswitch -background none
1785 tty1 Ss+ 0:00 /sbin/getty -8 38400 tty1
6865 pts/0 Ss 0:00 bash
6971 pts/1 Ss+ 0:00 /bin/bash
7255 pts/0 R 39:26 ./tsh -p
7256 pts/0 Z 0:00 [echo] <defunct>
7428 pts/0 S+ 0:00 make test11
7429 pts/0 S+ 0:00 /bin/sh -c ./sdriver.pl -t trace11.txt -s ./tsh -a "-p"
7430 pts/0 S+ 0:00 /usr/bin/perl ./sdriver.pl -t trace11.txt -s ./tsh -a -p
7431 pts/0 R+ 0:02 ./tsh -p
7436 pts/0 R 0:00 /bin/ps a
  • test12test13略(答案太长了)

test14

这个测试需要对fgbg的输入参数进行一些错误处理。例如没有参数或参数非数值或所选任务或进程不存在等。修改do_bgfg函数如。(/ ADD PART /带有这个标签的就是增加的功能)

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/*
* do_bgfg - Execute the builtin bg and fg commands
*/
void do_bgfg(char** argv)
{
struct job_t* job;
char* id = argv[1];

/* ADD PART bg和fg后边不跟任何参数不执行直接返回 */
// no argument for bg/fg
if (id == NULL)
{
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return;
}

// 判断输入的是jid还是pid
if (id[0] == '%') { /* jid */
/* ADD PART 检查输入的jid是不是数字 */
if (!checkNum(id + 1)) {
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
//去掉'%'开始读jid,根据jid返回这个子进程的结构指针
int jid = atoi(id + 1);
job = getjobjid(jobs, jid);
/* ADD PART 找不到输入的这个作业 */
if (job == NULL) {
printf("%%%d: No such job\n", jid);
return;
}
}
else { /* pid */
/* ADD PART 检查输入的pid是不是数字 */
if (!checkNum(id)) {
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
int pid = atoi(id);
job = getjobpid(jobs, pid);
/* ADD PART 找不到这个作业 */
if (job == NULL) {
printf("(%d): No such process\n", pid);
return;
}
}

/*
* kill不单是杀掉进程,还有发送信号的功能
* 这里唤醒job所在的组中的所有进程
* 就是唤醒这个stopped的子进程,以及它派生的孙子进程
*/
kill(-(job->pid), SIGCONT);

if (!strcmp(argv[0], "fg")) { // fg command
job->state = FG;
// 等待该前台作业终止
waitfg(job->pid);
}
else { // bg command
job->state = BG;
/* 切换到bg后打印作业信息 */
printf("[%d] (%d) %s", pid2jid(job->pid), job->pid, job->cmdline);
}
}

查看参考输出

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest14
./sdriver.pl -t trace14.txt -s ./tshref -a "-p"
#
# trace14.txt - Simple error handling
#
tsh> ./bogus
./bogus: Command not found
tsh> ./myspin 4 &
[1] (7521) ./myspin 4 &
tsh> fg
fg command requires PID or %jobid argument
tsh> bg
bg command requires PID or %jobid argument
tsh> fg a
fg: argument must be a PID or %jobid
tsh> bg a
bg: argument must be a PID or %jobid
tsh> fg 9999999
(9999999): No such process
tsh> bg 9999999
(9999999): No such process
tsh> fg %2
%2: No such job
tsh> fg %1
Job [1] (7521) stopped by signal 20
tsh> bg %2
%2: No such job
tsh> bg %1
[1] (7521) ./myspin 4 &
tsh> jobs
[1] (7521) Running ./myspin 4 &
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make test14
./sdriver.pl -t trace14.txt -s ./tsh -a "-p"
#
# trace14.txt - Simple error handling
#
tsh> ./bogus
tsh> ./myspin 4 &
[1] (7540) ./myspin 4 &
tsh> fg
fg command requires PID or %jobid argument
tsh> bg
bg command requires PID or %jobid argument
tsh> fg a
fg: argument must be a PID or %jobid
tsh> bg a
bg: argument must be a PID or %jobid
tsh> fg 9999999
(9999999): No such process
tsh> bg 9999999
(9999999): No such process
tsh> fg %2
%2: No such job
tsh> fg %1
Job [1] (7540) stopped by signal 20
tsh> bg %2
%2: No such job
tsh> bg %1
[1] (7540) ./myspin 4 &
tsh> jobs
[1] (7540) Running ./myspin 4 &

对于输入的不合法的pid不是数字(例如44行)或者没有没有参数(42行)或者单纯没有这个pid(48行),都会进行处理并返回而不是崩溃。这个功能的晚上增强了shell的鲁棒性。

和参考基本一致,通关~

test15 & test16

我们看看测试的要求

1
2
3
4
5
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest15
./sdriver.pl -t trace15.txt -s ./tshref -a "-p"
#
# trace15.txt - Putting it all together
#

测试15是要我们把所有的方法放在一起。这个都不要做,本来就在一个文件tsh.c下写的。。。

1
2
3
4
5
6
puitar@ubuntu:~/Desktop/LAB4/shlab-handout$ make rtest16
./sdriver.pl -t trace16.txt -s ./tshref -a "-p"
#
# trace16.txt - Tests whether the shell can handle SIGTSTP and SIGINT
# signals that come from other processes instead of the terminal.
#

这个测试要我们测试shell收到SIGINT或者SIGTSTP的来源不是中断而是其他进程是否仍然能够正常工作。这个也不需要做什么。原因有二

  • 我们前面的make testn的时候,就是把我们的tsh作为shell driver的子进程,然后驱动给我们的tsh发送测试指令。其中就有SIGINT和SIGTSTP,我们能过够通过前面的,肯定是对的。
  • 我们的tsh至始至终实现对的都是收到信号,然后对自己的孩子怎么处理。至于如何接受外部信号,是从中断收到还是其他的进程收到,我们实验过程中虽然没有关注到。但是我们在从unix的shell输入“./tsh”之后进入到tsh
    • 这个过程是内核捕捉到SIGINT等信号发送给我们的unix shell,然后unix shell会把这些信号有发送给tsh,然后tsh又把它发送给子进程们。
    • 关系如下图。

image-20220526221432408

还有一个小知识点:参考exit与_exit的区别,可以知道在fork出的child中要用_exit来退出,否则exit会调用用atexit注册的函数并刷新父进程的缓冲区。一般来说在一个main函数中只调用一次exit或return。

1
2
3
4
5
6
7
8
9
10
// in `eval`

/*
* 前台执行
* 如果没有这个可执行程序,那么直接终止这个子进程
*/
if (execve(argv[0], argv, environ) < 0) {
printf("%s: Command not found.\n", argv[0]);
_exit(1);
}

tsh.c文件综合

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
/*
* tsh - A tiny shell program with job control
*
* <Puitar>
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>

/* Misc manifest constants */
#define MAXLINE 1024 /* max line size */
#define MAXARGS 128 /* max args on a command line */
#define MAXJOBS 16 /* max jobs at any point in time */
#define MAXJID 1<<16 /* max job ID */

/* Job states */
#define UNDEF 0 /* undefined */
#define FG 1 /* running in foreground */
#define BG 2 /* running in background */
#define ST 3 /* stopped */

/*
* Jobs states: FG (foreground), BG (background), ST (stopped)
* Job state transitions and enabling actions:
* FG -> ST : ctrl-z
* ST -> FG : fg command
* ST -> BG : bg command
* BG -> FG : fg command
* At most 1 job can be in the FG state.
*/

/* Global variables */
extern char** environ; /* defined in libc */
char prompt[] = "tsh> "; /* command line prompt (DO NOT CHANGE) */
int verbose = 0; /* if true, print additional output */
int nextjid = 1; /* next job ID to allocate */
char sbuf[MAXLINE]; /* for composing sprintf messages */

struct job_t { /* The job struct */
pid_t pid; /* job PID */
int jid; /* job ID [1, 2, ...] */
int state; /* UNDEF, BG, FG, or ST */
char cmdline[MAXLINE]; /* command line */
};
struct job_t jobs[MAXJOBS]; /* The job list */
/* End global variables */


/* Function prototypes */

/* Here are the functions that you will implement */
void eval(char* cmdline);
int builtin_cmd(char** argv);
void do_bgfg(char** argv);
void waitfg(pid_t pid);

void sigchld_handler(int sig);
void sigtstp_handler(int sig);
void sigint_handler(int sig);

/* Here are helper routines that we've provided for you */
int parseline(const char* cmdline, char** argv);
void sigquit_handler(int sig);

void clearjob(struct job_t* job);
void initjobs(struct job_t* jobs);
int maxjid(struct job_t* jobs);
int addjob(struct job_t* jobs, pid_t pid, int state, char* cmdline);
int deletejob(struct job_t* jobs, pid_t pid);
pid_t fgpid(struct job_t* jobs);
struct job_t* getjobpid(struct job_t* jobs, pid_t pid);
struct job_t* getjobjid(struct job_t* jobs, int jid);
int pid2jid(pid_t pid);
void listjobs(struct job_t* jobs);

void usage(void);
void unix_error(char* msg);
void app_error(char* msg);
typedef void handler_t(int);
handler_t* Signal(int signum, handler_t* handler);

// wrapper functions get from csapp.h
void Sigprocmask(int how, const sigset_t* set, sigset_t* oldset);
void Sigemptyset(sigset_t* set);
void Sigaddset(sigset_t* set, int signum);

// Sio package declaration from csapp.h
/* Sio (Signal-safe I/O) routines */
ssize_t sio_puts(char s[]);
ssize_t sio_putl(long v);
void sio_error(char s[]);

/* Sio wrappers */
ssize_t Sio_puts(char s[]);
ssize_t Sio_putl(long v);
void Sio_error(char s[]);


/*
* main - The shell's main routine
*/
int main(int argc, char** argv)
{
char c;
char cmdline[MAXLINE];
int emit_prompt = 1; /* emit prompt (default) */

/* Redirect stderr to stdout (so that driver will get all output
* on the pipe connected to stdout) */
dup2(1, 2);

/* Parse the command line */
while ((c = getopt(argc, argv, "hvp")) != EOF) {
switch (c) {
case 'h': /* print help message */
usage();
break;
case 'v': /* emit additional diagnostic info */
verbose = 1;
break;
case 'p': /* don't print a prompt */
emit_prompt = 0; /* handy for automatic testing */
break;
default:
usage();
}
}

/* Install the signal handlers */

/* These are the ones you will need to implement */
Signal(SIGINT, sigint_handler); /* ctrl-c */
Signal(SIGTSTP, sigtstp_handler); /* ctrl-z */
Signal(SIGCHLD, sigchld_handler); /* Terminated or stopped child */

/* This one provides a clean way to kill the shell */
Signal(SIGQUIT, sigquit_handler);

/* Initialize the job list */
initjobs(jobs);

/* Execute the shell's read/eval loop */
while (1) {

/* Read command line */
if (emit_prompt) {
printf("%s", prompt);
fflush(stdout);
}
if ((fgets(cmdline, MAXLINE, stdin) == NULL) && ferror(stdin))
app_error("fgets error");
if (feof(stdin)) { /* End of file (ctrl-d) */
fflush(stdout);
exit(0);
}

/* Evaluate the command line */
eval(cmdline);
fflush(stdout);
fflush(stdout);
}

exit(0); /* control never reaches here */
}

/*
* eval - Evaluate the command line that the user has just typed in
*
* If the user has requested a built-in command (quit, jobs, bg or fg)
* then execute it immediately. Otherwise, fork a child process and
* run the job in the context of the child. If the job is running in
* the foreground, wait for it to terminate and then return. Note:
* each child process must have a unique process group ID so that our
* background children don't receive SIGINT (SIGTSTP) from the kernel
* when we type ctrl-c (ctrl-z) at the keyboard.
*/
void eval(char* cmdline)
{
char* argv[MAXARGS]; /* argv for execve() */
int bg; /* Should the job run in bg or fg? */
pid_t pid; /* Process id */
sigset_t mask, prev;

bg = parseline(cmdline, argv);

if (argv[0] == NULL)
return; /* Ignore empty lines */

// 这里参考课本520的方法
/*
* sigemptyset是设置一个信号集
* sigaddset是向mask中添加SIGCHLD信号
* sigprocmask是保存现在的信号集到prev然后根据第一个参数设置信号集为mask
* 第二个参数设置成当前阻塞信号集合
*/
Sigemptyset(&mask);
Sigaddset(&mask, SIGCHLD);


if (!builtin_cmd(argv)) {
Sigprocmask(SIG_BLOCK, &mask, &prev); /* 阻塞SIGCHLD */

if ((pid = fork()) == 0) { /* Child runs user job */
/* 给fork出来的子进程设置一个独立的组,
* 子进程是这个组的组长,组id为子进程的pid */
setpgid(0, 0);

/* 对于子进程并不需要阻塞SIGCHLD的信号 */
Sigprocmask(SIG_UNBLOCK, &prev, NULL); // unblock SIGCHLD

/*
* 前台执行
* 如果没有这个可执行程序,那么直接终止这个子进程
*/
if (execve(argv[0], argv, environ) < 0) {
printf("%s: Command not found.\n", argv[0]);
_exit(1);
}
}

addjob(jobs, pid, bg ? BG : FG, cmdline);
/* 已经加到jobs了,解除阻塞 */
Sigprocmask(SIG_SETMASK, &prev, NULL);


/* Parent waits for foreground job to terminate */
if (!bg) // 前台运行则等待子进程
waitfg(pid);
else // background
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
}
}

/*
* parseline - Parse the command line and build the argv array.
*
* Characters enclosed in single quotes are treated as a single
* argument. Return true if the user has requested a BG job, false if
* the user has requested a FG job.
*/
int parseline(const char* cmdline, char** argv)
{
static char array[MAXLINE]; /* holds local copy of command line */
char* buf = array; /* ptr that traverses command line */
char* delim; /* points to first space delimiter */
int argc; /* number of args */
int bg; /* background job? */

strcpy(buf, cmdline);
buf[strlen(buf) - 1] = ' '; /* replace trailing '\n' with space */
while (*buf && (*buf == ' ')) /* ignore leading spaces */
buf++;

/* Build the argv list */
argc = 0;
if (*buf == '\'') {
buf++;
delim = strchr(buf, '\'');
}
else {
delim = strchr(buf, ' ');
}

while (delim) {
argv[argc++] = buf;
*delim = '\0';
buf = delim + 1;
while (*buf && (*buf == ' ')) /* ignore spaces */
buf++;

if (*buf == '\'') {
buf++;
delim = strchr(buf, '\'');
}
else {
delim = strchr(buf, ' ');
}
}
argv[argc] = NULL;

if (argc == 0) /* ignore blank line */
return 1;

/* should the job run in the background? */
if ((bg = (*argv[argc - 1] == '&')) != 0) {
argv[--argc] = NULL;
}
return bg;
}

/*
* builtin_cmd - If the user has typed a built-in command then execute
* it immediately.
*/
int builtin_cmd(char** argv)
{
// quit command
if (!strcmp(argv[0], "quit"))
exit(0);

// jobs command
if (!strcmp(argv[0], "jobs")) {
listjobs(jobs);
return 1;
}

// bg or fg command
if (!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) {
do_bgfg(argv);
return 1;
}

// ignore singleton & (不处理单独的 '&')
if (!strcmp(argv[0], "&"))
return 1;

// not a build-in command
return 0;
}

// check if arg is a string of nums
int checkNum(char* arg) {
int len = strlen(arg);
int i;
for (i = 0; i < len; ++i) {
if (!isdigit(arg[i])) // not num
return 0;
}
return 1;
}

/*
* do_bgfg - Execute the builtin bg and fg commands
*/
void do_bgfg(char** argv)
{
struct job_t* job;
char* id = argv[1];

// no argument for bg/fg
if (id == NULL)
{
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return;
}

// 判断输入的是jid还是pid
if (id[0] == '%') { /* jid */
if (!checkNum(id + 1)) {
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
//去掉'%'开始读jid,根据jid返回这个子进程的结构指针
int jid = atoi(id + 1);
job = getjobjid(jobs, jid);
if (job == NULL) {
printf("%%%d: No such job\n", jid);
return;
}
}
else { /* pid */
if (!checkNum(id)) {
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
int pid = atoi(id);
job = getjobpid(jobs, pid);
if (job == NULL) {
printf("(%d): No such process\n", pid);
return;
}
}

/*
* kill不单是杀掉进程,还有发送信号的功能
* 这里唤醒job所在的组中的所有进程
* 就是唤醒这个stopped的子进程,以及它派生的孙子进程
*/
kill(-(job->pid), SIGCONT);

if (!strcmp(argv[0], "fg")) { // fg command
job->state = FG;
// 等待该前台作业终止
waitfg(job->pid);
}
else { // bg command
job->state = BG;
/* 切换到bg后打印作业信息 */
printf("[%d] (%d) %s", pid2jid(job->pid), job->pid, job->cmdline);
}
}

/*
* waitfg - Block until process pid is no longer the foreground process
*/
void waitfg(pid_t pid)
{
while (pid == fgpid(jobs))
sleep(0); // 这里是主动让出CPU
}

/*****************
* Signal handlers
*****************/

/*
* sigchld_handler - The kernel sends a SIGCHLD to the shell whenever
* a child job terminates (becomes a zombie), or stops because it
* received a SIGSTOP or SIGTSTP signal. The handler reaps all
* available zombie children, but doesn't wait for any other
* currently running children to terminate.
*/
void sigchld_handler(int sig)
{
int old_errno = errno;

pid_t pid;
int status;

if (verbose)
printf("sigchld_handler: entering \n");

/*
* 回收僵尸进程
* 这里的WNOHANG是非常重要的。
* 它的本意是如果所有孩子都没有僵尸(终止)状态的,直接退出
* 这个能够避免在这里等待所有前台的running和stopped程序终止
* 这样tsh就不能正常接受用户的输入了
* WUNTRACED是等待直到有一个子进程变成僵尸退出,返回它的pid
*/
while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
// 子进程正常退出
if (WIFEXITED(status)) {
deletejob(jobs, pid);
}
// 子进程因为ctrl-c退出
if (WIFSIGNALED(status)) { // terminated by ctrl-c
/* printf("Job [%d] (%d) terminated by signal %2d\n",
pid2jid(pid), pid, WTERMSIG(status)); */ {
Sio_puts("Job [");
Sio_putl(pid2jid(pid));
Sio_puts("] (");
Sio_putl(pid);
Sio_puts(") terminated by signal ");
Sio_putl(WTERMSIG(status));
Sio_puts("\n");
}
deletejob(jobs, pid);
}
if (WIFSTOPPED(status)) { // stopped by ctrl-z
/* printf("Job [%d] (%d) stopped by signal %2d\n",
pid2jid(pid), pid, WSTOPSIG(status)); */ {
Sio_puts("Job [");
Sio_putl(pid2jid(pid));
Sio_puts("] (");
Sio_putl(pid);
Sio_puts(") stopped by signal ");
Sio_putl(WSTOPSIG(status));
Sio_puts("\n");
}
// 修改子进程状态为ST
getjobpid(jobs, pid)->state = ST;
}
}
errno = old_errno;
if (verbose) {
/* printf("sigchld_handler: exiting\n"); */ {
Sio_puts("sigchld_handler: exiting\n");
}
}
}

/*
* sigint_handler - The kernel sends a SIGINT to the shell whenver the
* user types ctrl-c at the keyboard. Catch it and send it along
* to the foreground job.
*/
void sigint_handler(int sig)
{
int olderrno = errno;
// get the foreground job pid
pid_t fg_pid = fgpid(jobs);
// kill the group in the foreground
/*
* int kill(pid_t pid,int signo)
* 功能: 向进程或进程组发送一个信号 (成功返回 0; 否则,返回 -1 )
*/
kill(-fg_pid, sig);
// -fg_pid表示向进程组号为pid的组中的每个进程发sig信号
// 在这里就是向前台进程以及它的每一个子进程(子进程都在自己的父进程的pid为组id的组下)
errno = olderrno;
}

/*
* sigtstp_handler - The kernel sends a SIGTSTP to the shell whenever
* the user types ctrl-z at the keyboard. Catch it and suspend the
* foreground job by sending it a SIGTSTP.
*/
void sigtstp_handler(int sig)
{
// 为了异步信号安全防止errno被覆盖
int olderrno = errno;
// get the foreground job pid
pid_t fg_pid = fgpid(jobs);
// kill the group in the foreground
kill(-fg_pid, sig);
errno = olderrno;
}

/*********************
* End signal handlers
*********************/

/***********************************************
* Helper routines that manipulate the job list
**********************************************/

/* clearjob - Clear the entries in a job struct */
void clearjob(struct job_t* job) {
job->pid = 0;
job->jid = 0;
job->state = UNDEF;
job->cmdline[0] = '\0';
}

/* initjobs - Initialize the job list */
void initjobs(struct job_t* jobs) {
int i;

for (i = 0; i < MAXJOBS; i++)
clearjob(&jobs[i]);
}

/* maxjid - Returns largest allocated job ID */
int maxjid(struct job_t* jobs)
{
int i, max = 0;

for (i = 0; i < MAXJOBS; i++)
if (jobs[i].jid > max)
max = jobs[i].jid;
return max;
}

/* addjob - Add a job to the job list */
int addjob(struct job_t* jobs, pid_t pid, int state, char* cmdline)
{
int i;

if (pid < 1)
return 0;

for (i = 0; i < MAXJOBS; i++) {
/* 如果jobs数组里面有pid等于0等于0的表项,表示这个位置可以用,把这个作业放进去 */
if (jobs[i].pid == 0) {
jobs[i].pid = pid;
jobs[i].state = state;
jobs[i].jid = nextjid++;
if (nextjid > MAXJOBS)
nextjid = 1;
strcpy(jobs[i].cmdline, cmdline);
if (verbose) {
printf("Added job [%d] %d %s\n", jobs[i].jid, jobs[i].pid, jobs[i].cmdline);
}
return 1;
}
}
printf("Tried to create too many jobs\n");
return 0;
}

/* deletejob - Delete a job whose PID=pid from the job list */
int deletejob(struct job_t* jobs, pid_t pid)
{
int i;

if (pid < 1)
return 0;

for (i = 0; i < MAXJOBS; i++) {
if (jobs[i].pid == pid) {
clearjob(&jobs[i]);
nextjid = maxjid(jobs) + 1;
return 1;
}
}
return 0;
}

/* fgpid - Return PID of current foreground job, 0 if no such job */
pid_t fgpid(struct job_t* jobs) {
int i;

for (i = 0; i < MAXJOBS; i++)
if (jobs[i].state == FG)
return jobs[i].pid;
return 0;
}

/* getjobpid - Find a job (by PID) on the job list */
struct job_t* getjobpid(struct job_t* jobs, pid_t pid) {
int i;

if (pid < 1)
return NULL;
for (i = 0; i < MAXJOBS; i++)
if (jobs[i].pid == pid)
return &jobs[i];
return NULL;
}

/* getjobjid - Find a job (by JID) on the job list */
struct job_t* getjobjid(struct job_t* jobs, int jid)
{
int i;

if (jid < 1)
return NULL;
for (i = 0; i < MAXJOBS; i++)
if (jobs[i].jid == jid)
return &jobs[i];
return NULL;
}

/* pid2jid - Map process ID to job ID */
int pid2jid(pid_t pid)
{
int i;

if (pid < 1)
return 0;
for (i = 0; i < MAXJOBS; i++)
if (jobs[i].pid == pid) {
return jobs[i].jid;
}
return 0;
}

/* listjobs - Print the job list */
void listjobs(struct job_t* jobs)
{
int i;

for (i = 0; i < MAXJOBS; i++) {
if (jobs[i].pid != 0) {
printf("[%d] (%d) ", jobs[i].jid, jobs[i].pid);
switch (jobs[i].state) {
case BG:
printf("Running ");
break;
case FG:
printf("Foreground ");
break;
case ST:
printf("Stopped ");
break;
default:
printf("listjobs: Internal error: job[%d].state=%d ",
i, jobs[i].state);
}
printf("%s", jobs[i].cmdline);
}
}
}
/******************************
* end job list helper routines
******************************/


/***********************
* Other helper routines
***********************/

/*
* usage - print a help message
*/
void usage(void)
{
printf("Usage: shell [-hvp]\n");
printf(" -h print this message\n");
printf(" -v print additional diagnostic information\n");
printf(" -p do not emit a command prompt\n");
exit(1);
}

/*
* unix_error - unix-style error routine
*/
void unix_error(char* msg)
{
fprintf(stdout, "%s: %s\n", msg, strerror(errno));
exit(1);
}

/*
* app_error - application-style error routine
*/
void app_error(char* msg)
{
fprintf(stdout, "%s\n", msg);
exit(1);
}

/*
* Signal - wrapper for the sigaction function
*/
handler_t* Signal(int signum, handler_t* handler)
{
struct sigaction action, old_action;

action.sa_handler = handler;
sigemptyset(&action.sa_mask); /* block sigs of type being handled */
action.sa_flags = SA_RESTART; /* restart syscalls if possible */

if (sigaction(signum, &action, &old_action) < 0)
unix_error("Signal error");
return (old_action.sa_handler);
}

/*
* sigquit_handler - The driver program can gracefully terminate the
* child shell by sending it a SIGQUIT signal.
*/
void sigquit_handler(int sig)
{
printf("Terminating after receipt of SIGQUIT signal\n");
exit(1);
}


// The following function are from csapp.c

/*************************************************************
* Wrappers for blocking signals
*************************************************************/

void Sigemptyset(sigset_t* set)
{
if (sigemptyset(set) < 0)
unix_error("Sigemptyset error");
return;
}

void Sigprocmask(int how, const sigset_t* set, sigset_t* oldset)
{
if (sigprocmask(how, set, oldset) < 0)
unix_error("Sigprocmask error");
return;
}

void Sigaddset(sigset_t* set, int signum)
{
if (sigaddset(set, signum) < 0)
unix_error("Sigaddset error");
return;
}

/*************************************************************
* The Sio (Signal-safe I/O) package - simple reentrant output
* functions that are safe for signal handlers.
*************************************************************/

/* Private sio functions */

/* $begin sioprivate */
/* sio_reverse - Reverse a string (from K&R) */
static void sio_reverse(char s[])
{
int c, i, j;

for (i = 0, j = strlen(s) - 1; i < j; i++, j--) {
c = s[i];
s[i] = s[j];
s[j] = c;
}
}

/* sio_ltoa - Convert long to base b string (from K&R) */
static void sio_ltoa(long v, char s[], int b)
{
int c, i = 0;
int neg = v < 0;

if (neg)
v = -v;

do {
s[i++] = ((c = (v % b)) < 10) ? c + '0' : c - 10 + 'a';
} while ((v /= b) > 0);

if (neg)
s[i++] = '-';

s[i] = '\0';
sio_reverse(s);
}

/* sio_strlen - Return length of string (from K&R) */
static size_t sio_strlen(char s[])
{
int i = 0;

while (s[i] != '\0')
++i;
return i;
}
/* $end sioprivate */

/* Public Sio functions */
/* $begin siopublic */

ssize_t sio_puts(char s[]) /* Put string */
{
return write(STDOUT_FILENO, s, sio_strlen(s)); //line:csapp:siostrlen
}

ssize_t sio_putl(long v) /* Put long */
{
char s[128];

sio_ltoa(v, s, 10); /* Based on K&R itoa() */ //line:csapp:sioltoa
return sio_puts(s);
}

void sio_error(char s[]) /* Put error message and exit */
{
sio_puts(s);
_exit(1); //line:csapp:sioexit
}
/* $end siopublic */

/*******************************
* Wrappers for the SIO routines
******************************/
ssize_t Sio_putl(long v)
{
ssize_t n;

if ((n = sio_putl(v)) < 0)
sio_error("Sio_putl error");
return n;
}

ssize_t Sio_puts(char s[])
{
ssize_t n;

if ((n = sio_puts(s)) < 0)
sio_error("Sio_puts error");
return n;
}

void Sio_error(char s[])
{
sio_error(s);
}

实验总结

通过这次实验,才算是比较彻底理解了,信号的相关知识。

  • 包括信号的发送和捕捉
  • 异步的思考方式
  • 信号的阻塞
  • 拓展:异步安全问题
  • 信号处理程序的编写和使用
  • signal函数和kill以及相关的函数的使用等

这个是本课程最后一个实验的,综合难度并不是最大的。但是是知识体系最为复杂的。总的来说非常喜欢这门课的实验。做完之后能够感觉到和知识的紧密连接。感谢陪伴~

操作系统第三部分作业

作业范围

  • 第26章1、2、3、4题
  • 第28章1、2、3、4、5、6、7题
  • 第30章1、2、4、8、9、10、11题
  • 第31章1、2、4、5、6题

第26章

1.

首先查看这个程序,得到结果如下

1
2
3
4
5
6
7
8
9
sh> cat loop.s
# 段符号
.main
.top
# 重点看下面这4句
sub $1,%dx # %dx = %dx - 1
test $0,%dx # %dx & $0 不改变%dx的值,仅仅改变标志位
jgte .top # 表示如果大于等于就跳到.top
halt # 停机

现在我们理解了loop.s句子的含义。现在我们看看题目,假设dx的初始值是0,很快就可以计算出每个指令执行的时候dx的值。

1
2
3
4
5
6
7
8
$ ./x86.py -p loop.s -t 1 -i 100 -R dx
...
dx Thread 0
0
-1 1000 sub $1,%dx
-1 1001 test $0,%dx
-1 1002 jgte .top
-1 1003 halt

dx作为循环变量判断是否跳出循环,上述语句可以写成c语言的语句如下:

1
2
3
4
int i = n;
while (i >= 0) {
i--;
}

所以它的价值就在于循环递减变量以及循环变量判断是否跳出循环。

2.

现在运行相同的代码,但是使用如下标志,但是设定了寄存器的初始值如-a后面所赋值。

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
$ ./x86.py -p loop.s -t 2 -i 100 -a dx=3,dx=3 -R dx
...
dx Thread 0 Thread 1
3
2 1000 sub $1,%dx
2 1001 test $0,%dx
2 1002 jgte .top
1 1000 sub $1,%dx
1 1001 test $0,%dx
1 1002 jgte .top
0 1000 sub $1,%dx
0 1001 test $0,%dx
0 1002 jgte .top
-1 1000 sub $1,%dx
-1 1001 test $0,%dx
-1 1002 jgte .top
-1 1003 halt
3 ----- Halt;Switch ----- ----- Halt;Switch -----
2 1000 sub $1,%dx
2 1001 test $0,%dx
2 1002 jgte .top
1 1000 sub $1,%dx
1 1001 test $0,%dx
1 1002 jgte .top
0 1000 sub $1,%dx
0 1001 test $0,%dx
0 1002 jgte .top
-1 1000 sub $1,%dx
-1 1001 test $0,%dx
-1 1002 jgte .top
-1 1003 halt

在这种情况下,多线程不会影响结果的运行,因为两个线程执行的时候不是共享同一个edx的。这种情况下,两个线程只使用自己的dx的值,dx在每个线程从3到-1。并且在中断前halt跳出循环。

至于这段代码是否有竞态条件?首先要弄清楚什么叫竞态条件等两个基本概念。

  • 临界区:访问共享资源的一段代码,资源通常是一个变量或数据结构。
  • 竞态条件:出现在多个执行线程同时进入临界区时,他们都试图更新共享的数据结构导致了不确定结果

这一段loop.s中对共享资源更新的部分的代码就是sub指令这一条。因此因此我们可以看临界区就是这一段loop.s或者就是sub这一句。很明显从在这种情况下,线程每100条指令中断1次,而还没到100条指令就已经跳出循环了。这种情况下可以看出来,两段代码是没有竞态条件的。

3.

这一组测试使得中断间隔非常小而且随机,使用不同的种子-s查看不同的交替。中断频率是否会改变这个程序的行为。

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
42
$ ./x86.py -p loop.s -t 2 -i 3 -r -a dx=3,dx=3 -R dx
...
dx Thread 0 Thread 1
3
2 1000 sub $1,%dx
2 1001 test $0,%dx
2 1002 jgte .top
3 ------ Interrupt ------ ------ Interrupt ------
2 1000 sub $1,%dx
2 1001 test $0,%dx
2 1002 jgte .top
2 ------ Interrupt ------ ------ Interrupt ------
1 1000 sub $1,%dx
1 1001 test $0,%dx
2 ------ Interrupt ------ ------ Interrupt ------
1 1000 sub $1,%dx
1 ------ Interrupt ------ ------ Interrupt ------
1 1002 jgte .top
0 1000 sub $1,%dx
1 ------ Interrupt ------ ------ Interrupt ------
1 1001 test $0,%dx
1 1002 jgte .top
0 ------ Interrupt ------ ------ Interrupt ------
0 1001 test $0,%dx
0 1002 jgte .top
-1 1000 sub $1,%dx
1 ------ Interrupt ------ ------ Interrupt ------
0 1000 sub $1,%dx
-1 ------ Interrupt ------ ------ Interrupt ------
-1 1001 test $0,%dx
-1 1002 jgte .top
0 ------ Interrupt ------ ------ Interrupt ------
0 1001 test $0,%dx
0 1002 jgte .top
-1 ------ Interrupt ------ ------ Interrupt ------
-1 1003 halt
0 ----- Halt;Switch ----- ----- Halt;Switch -----
-1 1000 sub $1,%dx
-1 1001 test $0,%dx
-1 ------ Interrupt ------ ------ Interrupt ------
-1 1002 jgte .top
-1 1003 halt

再看看不同的随机种子下是什么样的结果。

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
42
43
$ ./x86.py -p loop.s -t 2 -i 3 -r -a dx=3,dx=3 -R dx -c -s 1
...
dx Thread 0 Thread 1
3
2 1000 sub $1,%dx
3 ------ Interrupt ------ ------ Interrupt ------
2 1000 sub $1,%dx
2 1001 test $0,%dx
2 1002 jgte .top
2 ------ Interrupt ------ ------ Interrupt ------
2 1001 test $0,%dx
2 1002 jgte .top
1 1000 sub $1,%dx
2 ------ Interrupt ------ ------ Interrupt ------
1 1000 sub $1,%dx
1 ------ Interrupt ------ ------ Interrupt ------
1 1001 test $0,%dx
1 1002 jgte .top
1 ------ Interrupt ------ ------ Interrupt ------
1 1001 test $0,%dx
1 1002 jgte .top
1 ------ Interrupt ------ ------ Interrupt ------
0 1000 sub $1,%dx
0 1001 test $0,%dx
1 ------ Interrupt ------ ------ Interrupt ------
0 1000 sub $1,%dx
0 1001 test $0,%dx
0 1002 jgte .top
0 ------ Interrupt ------ ------ Interrupt ------
0 1002 jgte .top
0 ------ Interrupt ------ ------ Interrupt ------
-1 1000 sub $1,%dx
0 ------ Interrupt ------ ------ Interrupt ------
-1 1000 sub $1,%dx
-1 1001 test $0,%dx
-1 1002 jgte .top
-1 ------ Interrupt ------ ------ Interrupt ------
-1 1001 test $0,%dx
-1 1002 jgte .top
-1 ------ Interrupt ------ ------ Interrupt ------
-1 1003 halt
-1 ----- Halt;Switch ----- ----- Halt;Switch -----
-1 1003 halt
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
42
43
./x86.py -p loop.s -t 2 -i 3 -r -a dx=3,dx=3 -R dx -c -s 2
...
dx Thread 0 Thread 1
3
2 1000 sub $1,%dx
2 1001 test $0,%dx
2 1002 jgte .top
3 ------ Interrupt ------ ------ Interrupt ------
2 1000 sub $1,%dx
2 1001 test $0,%dx
2 1002 jgte .top
2 ------ Interrupt ------ ------ Interrupt ------
1 1000 sub $1,%dx
2 ------ Interrupt ------ ------ Interrupt ------
1 1000 sub $1,%dx
1 ------ Interrupt ------ ------ Interrupt ------
1 1001 test $0,%dx
1 1002 jgte .top
0 1000 sub $1,%dx
1 ------ Interrupt ------ ------ Interrupt ------
1 1001 test $0,%dx
1 1002 jgte .top
0 1000 sub $1,%dx
0 ------ Interrupt ------ ------ Interrupt ------
0 1001 test $0,%dx
0 1002 jgte .top
-1 1000 sub $1,%dx
0 ------ Interrupt ------ ------ Interrupt ------
0 1001 test $0,%dx
-1 ------ Interrupt ------ ------ Interrupt ------
-1 1001 test $0,%dx
-1 1002 jgte .top
0 ------ Interrupt ------ ------ Interrupt ------
0 1002 jgte .top
-1 1000 sub $1,%dx
-1 ------ Interrupt ------ ------ Interrupt ------
-1 1003 halt
-1 ----- Halt;Switch ----- ----- Halt;Switch -----
-1 1001 test $0,%dx
-1 ------ Interrupt ------ ------ Interrupt ------
-1 1002 jgte .top
-1 ------ Interrupt ------ ------ Interrupt ------
-1 1003 halt

总体看来,两个线程仍然是执行了从3到-1的循环操作,但是由于中断具有随机性,因此也就不能够确定什么时候执行完毕。从工作执行总量上看没有发生改变,但就工作执行的先后以及连续性上,改变了行为。

4.

第四题是访问一个公共变量而不是寄存器,在前面3题中,每个线程都有自己的专属寄存器。在中断的时候,线程的寄存器会保存,在恢复中断恢复的时候寄存器的值会被恢复。因此在循环上不会被破坏。但是现在的公共资源是一个变量,这可能引起某些变化。具体见下面。

首先看看这个程序looping-race-nolock.s是什么意思

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ cat looping-race-nolock.s
# assumes %bx has loop count in it

.main
.top
# critical section 临界区
mov 2000, %ax # get 'value' at address 2000
add $1, %ax # increment it: ax += 1
mov %ax, 2000 # store it back

# see if we're still looping
sub $1, %bx
test $0, %bx
jgt .top

halt

可以看出来bx是循环变量,要对ax进行加一操作,可以写出程序的c语言表达如下

1
2
3
4
5
6
7
8
int i = n;
int x;
int *p;
while (i >= 0) {
*p = x;
*p--;
i--;
}

然后我们再来看看下面这段执行情况。显然除了mov %ax 2000这个语句会更新2000地址的变量x的值,其余时刻都不会更新。因此结果如下。

1
2
3
4
5
6
7
8
9
10
11
$ ./x86.py -p looping-race-nolock.s -t 1 -M 2000
...
2000 Thread 0
0
0 1000 mov 2000, %ax
0 1001 add $1, %ax
1 1002 mov %ax, 2000
1 1003 sub $1, %bx
1 1004 test $0, %bx
1 1005 jgt .top
1 1006 halt

第28章

这一章的习题同样是一个用python写的x86模拟器,它能够运行.s格式的文件。

在这里,我们有四个常用的寄存器ax、bx、cx、dx,还有一个程序计数器PC。还有足够小但是能够满足我们需求的汇编指令。我们也加了一些额外的寄存器,例如ex和fx,这不见得能够与x86是对应的。但是这并没有什么问题。

1.

这题要我们用./x86运行flag.s文件。首先理解一下flag.s到底说了什么。

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
$ cat flag.s
.var flag
.var count

.main
.top

.acquire
mov flag, %ax # get flag
test $0, %ax # if we get 0 back: lock is free!
jne .acquire # if not, try again
mov $1, flag # store 1 into flag

# critical section
mov count, %ax # get the value at the address
add $1, %ax # increment it
mov %ax, count # store it back

# release lock
mov $0, flag # clear the flag now

# see if we're still looping
sub $1, %bx
test $0, %bx
jgt .top

halt

上面这部分汇编代码其实很好理解,根据上面的注释,我们可以写出c语言版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typedef char mutex_t;
mutex_t flag = 0x0;
int count = 0;
int i = n;

int
mian() {
do {
/* 上锁 */
while (flag != 0x0)
; // 自选等待锁空闲
/* lock */
flag = 0x1;
/* 临界区代码 */
count++;
/* unlock */
flag = 0x0;
/* 判断是否继续循环 */
i--;
} while (i > 0);
/* halt */
exit(1);
}

所以flag.s的功能就是在循环变量被减到0之前不断申请上锁,然后对公共资源count++,然后释放锁的过程。

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
$./x86.py -p flag.s
...
Thread 0 Thread 1

1000 mov flag, %ax
1001 test $0, %ax
1002 jne .acquire
1003 mov $1, flag
1004 mov count, %ax
1005 add $1, %ax
1006 mov %ax, count
1007 mov $0, flag
1008 sub $1, %bx
1009 test $0, %bx
1010 jgt .top
1011 halt
----- Halt;Switch ----- ----- Halt;Switch -----
1000 mov flag, %ax
1001 test $0, %ax
1002 jne .acquire
1003 mov $1, flag
1004 mov count, %ax
1005 add $1, %ax
1006 mov %ax, count
1007 mov $0, flag
1008 sub $1, %bx
1009 test $0, %bx
1010 jgt .top
1011 halt

和上面的26的作业类似,在中断时间片较短的时候,两个线程都能够完成各自的工作。这个时候,count能够获得两个线程的累加工作,并获得确定的答案。既能够获得某个线程两倍的累加量。

2.

上面其实已经解释了。答案是会的,能够按照预期工作。我们可以详细跟踪各个变量和寄存器的值看看

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
$./x86.py -p flag.s -R ax,bx -M flag,count -c
...
flag count ax bx Thread 0 Thread 1

0 0 0 0
0 0 0 0 1000 mov flag, %ax
0 0 0 0 1001 test $0, %ax
0 0 0 0 1002 jne .acquire
1 0 0 0 1003 mov $1, flag
1 0 0 0 1004 mov count, %ax
1 0 1 0 1005 add $1, %ax
1 1 1 0 1006 mov %ax, count
0 1 1 0 1007 mov $0, flag
0 1 1 -1 1008 sub $1, %bx
0 1 1 -1 1009 test $0, %bx
0 1 1 -1 1010 jgt .top
0 1 1 -1 1011 halt
0 1 0 0 ----- Halt;Switch ----- ----- Halt;Switch -----
0 1 0 0 1000 mov flag, %ax
0 1 0 0 1001 test $0, %ax
0 1 0 0 1002 jne .acquire
1 1 0 0 1003 mov $1, flag
1 1 1 0 1004 mov count, %ax
1 1 2 0 1005 add $1, %ax
1 2 2 0 1006 mov %ax, count
0 2 2 0 1007 mov $0, flag
0 2 2 -1 1008 sub $1, %bx
0 2 2 -1 1009 test $0, %bx
0 2 2 -1 1010 jgt .top
0 2 2 -1 1011 halt

可以看到两个线程的循环变量寄存器都是bx,它们都是1,说明可以各自累加一次。count的初始值是0,所以最终各自累加一次,可以看到最后count的值恰好是2,证明了可以按照预期工作。

3.

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
42
43
44
45
46
47
48
49
50
51
52
53
$./x86.py -p flag.s -R ax,bx -M flag,count -c -a bx=2,bx=2
...
flag count ax bx Thread 0 Thread 1

0 0 0 2
0 0 0 2 1000 mov flag, %ax
0 0 0 2 1001 test $0, %ax
0 0 0 2 1002 jne .acquire
1 0 0 2 1003 mov $1, flag
1 0 0 2 1004 mov count, %ax
1 0 1 2 1005 add $1, %ax
1 1 1 2 1006 mov %ax, count
0 1 1 2 1007 mov $0, flag
0 1 1 1 1008 sub $1, %bx
0 1 1 1 1009 test $0, %bx
0 1 1 1 1010 jgt .top
0 1 0 1 1000 mov flag, %ax
0 1 0 1 1001 test $0, %ax
0 1 0 1 1002 jne .acquire
1 1 0 1 1003 mov $1, flag
1 1 1 1 1004 mov count, %ax
1 1 2 1 1005 add $1, %ax
1 2 2 1 1006 mov %ax, count
0 2 2 1 1007 mov $0, flag
0 2 2 0 1008 sub $1, %bx
0 2 2 0 1009 test $0, %bx
0 2 2 0 1010 jgt .top
0 2 2 0 1011 halt
0 2 0 2 ----- Halt;Switch ----- ----- Halt;Switch -----
0 2 0 2 1000 mov flag, %ax
0 2 0 2 1001 test $0, %ax
0 2 0 2 1002 jne .acquire
1 2 0 2 1003 mov $1, flag
1 2 2 2 1004 mov count, %ax
1 2 3 2 1005 add $1, %ax
1 3 3 2 1006 mov %ax, count
0 3 3 2 1007 mov $0, flag
0 3 3 1 1008 sub $1, %bx
0 3 3 1 1009 test $0, %bx
0 3 3 1 1010 jgt .top
0 3 0 1 1000 mov flag, %ax
0 3 0 1 1001 test $0, %ax
0 3 0 1 1002 jne .acquire
1 3 0 1 1003 mov $1, flag
1 3 3 1 1004 mov count, %ax
1 3 4 1 1005 add $1, %ax
1 4 4 1 1006 mov %ax, count
0 4 4 1 1007 mov $0, flag
0 4 4 0 1008 sub $1, %bx
0 4 4 0 1009 test $0, %bx
0 4 4 0 1010 jgt .top
0 4 4 0 1011 halt

和前面的题目一样,实际就是两个线程分别进行两次累加,可以看到到目前为止,都能够正确完成累加。原因是中断切换线程执行的时候,两个线程都能够完成所有的累加,就不会出现存在竞态条件的情况。

4.

这里我让循环为50次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$./x86.py -p flag.s -R ax,bx -M flag,count -c -a bx=50,bx=50 -i 3
...
flag count ax bx Thread 0 Thread 1

0 0 0 2
0 0 0 2 1000 mov flag, %ax
0 0 0 2 1001 test $0, %ax
0 0 0 2 1002 jne .acquire
1 0 0 2 1003 mov $1, flag
1 0 0 2 1004 mov count, %ax
1 0 1 2 1005 add $1, %ax
1 1 1 2 1006 mov %ax, count
0 1 1 2 1007 mov $0, flag
...
1 57 57 1 1006 mov %ax, count
0 57 57 1 1007 mov $0, flag
0 57 57 1 ------ Interrupt ------ ------ Interrupt ------
0 57 57 0 1008 sub $1, %bx
0 57 57 0 1009 test $0, %bx
0 57 57 0 1010 jgt .top
0 57 57 0 ------ Interrupt ------ ------ Interrupt ------
0 57 57 0 1011 halt

你能够看到两个线程循环50次,应该要能够累加100次,但是最后的结果是只累加了57次。此时我们说出现了不确定现象。

由于以flag作为锁,它的加锁和解锁本身没有原子性,所以并不能够起到保护共享资源的作用。所以随着bx增大,i减小,越容易产生不好的结果。反过来bx减小,i增大越趋向于产生确定的好的结果。

5.

现在我们研究另外一个文件,它是个使用硬件原语test-and-set的指令,详细看看:

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
$ cat test-and-set.s
.var mutex
.var count

.main
.top

.acquire
mov $1, %ax
xchg %ax, mutex # atomic swap of 1 and mutex
test $0, %ax # if we get 0 back: lock is free!
jne .acquire # if not, try again

# critical section
mov count, %ax # get the value at the address
add $1, %ax # increment it
mov %ax, count # store it back

# release lock
mov $0, mutex

# see if we're still looping
sub $1, %bx
test $0, %bx
jgt .top

halt

同样针对上面的汇编代码我们写出c语言版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef char mutex_t;
mutex_t mutex;
int count = 0;
int i = n;

int
main() {
do {
/* 等待锁空闲,如果有空闲锁就上锁 */
while (test_and_set(&mutex, 0x1) != 0x0)
; // spin
/* 临界区 */
count++;
/* 解锁 */
mutex = 0x0;
i--;
} while (i > 0);
/* halt */
exit(1);
}

其中获取锁部分:

1
2
3
4
5
6
7
8
9
/* 等待锁空闲,如果有空闲锁就上锁 */
while (test_and_set(&mutex, 0x1) != 0x0)
; // spin
=>
.acquire
mov $1, %ax
xchg %ax, mutex # atomic swap of 1 and mutex
test $0, %ax # if we get 0 back: lock is free!
jne .acquire # if not, try again

释放锁部分:

1
2
3
4
/* 解锁 */
mutex = 0x0;
=>
mov $0, mutex

6.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ ./x86.py -p test-and-set.s -i 3 -a bx=50,bx=50 -M mutex,count -R ax,bx -c
...
mutex count ax bx Thread 0 Thread 1

0 0 0 50
0 0 1 50 1000 mov $1, %ax
1 0 0 50 1001 xchg %ax, mutex
1 0 0 50 1002 test $0, %ax
1 0 0 50 ------ Interrupt ------ ------ Interrupt ------
1 0 1 50 1000 mov $1, %ax
1 0 1 50 1001 xchg %ax, mutex
1 0 1 50 1002 test $0, %ax
1 0 0 50 ------ Interrupt ------ ------ Interrupt ------
...
0 100 100 0 1008 sub $1, %bx
0 100 100 0 ------ Interrupt ------ ------ Interrupt ------
0 100 100 0 1009 test $0, %bx
0 100 100 0 1010 jgt .top
0 100 100 0 1011 halt

可以看到在这种test-and-set的硬件原语支持下,能够保证原子性。所以最后计算的结果能够满足预期。但是可以从上面的代码中的看出,虽然保证了加锁的原子性,进而控制了了每次只能有一个线程在临界区。但是,由于当线程没有抢占到锁,就会在while中自旋等待,倘若拥有锁定的线程迟迟不释放锁,其他大量的线程就可能都处于自选等待的状态。而如果线程间调度采用的是类似轮转这样的算法,那么就会有大量的CPU资源用来自选。这种自选显然不是我们想要的,因此由于分时共享CPU的存在,CPU的使用率虽然比较高,但是做的有用功却不是很高,大部分时间在做自选等待。

现在考虑如何量化这样的CPU使用率的描述。就比如上面的示例代码两个线程一共做了100次累加,每次累加只有下面3条指令是经行累加的

1
2
3
4
# critical section
mov count, %ax # get the value at the address
add $1, %ax # increment it
mov %ax, count # store it back

因此也就只有3*100 = 300条指令做有用功,剩下的通通是无用功。可以统计上面的计算共有2010行。由于整个计算过程CPU都处于工作状态。因此使用率100%。但是有效使用率仅有300/2010 = 14.93%

7.

首先我们需要通过增加-P属性来规定执行的顺序。例如:-P 1100111000111000111000就会依次按照t1,t1,t0,t0,t1,…,t0,t0,t0的顺序执行线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ ./x86.py -p test-and-set.s -i 10 -R ax,bx -M mutex,count -a bx=5 -P 1100111000111000111000 -c
...
mutex count ax bx Thread 0 Thread 1

0 0 0 5
0 0 1 5 1000 mov $1, %ax
1 0 0 5 1001 xchg %ax, mutex
1 0 0 5 ------ Interrupt ------ ------ Interrupt ------
1 0 1 5 1000 mov $1, %ax
1 0 1 5 1001 xchg %ax, mutex
1 0 0 5 ------ Interrupt ------ ------ Interrupt ------
1 0 0 5 1002 test $0, %ax
1 0 0 5 1003 jne .acquire
...
1 10 10 1 1006 mov %ax, count
0 10 10 1 1007 mov $0, mutex
0 10 10 0 1008 sub $1, %bx
0 10 10 0 1009 test $0, %bx
0 10 10 0 1010 jgt .top
0 10 10 0 1011 halt

可以看到最后结果是正确的,由于上面的获取锁过程是具有原子性的,也就是说每一次只能有一个线程执行成功(成功把flag从0变成1)获得锁。因此也就保证了正确性。

第30章

由于第30章中文版课本上没有题目,因此在这里放出英文课本原题。

1.

image-20220516082241930

这个题目要我们专注于文件main-two-cvs-while.c。首先需要先了解代码的含义,理解代码运行会发生什么。

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
/* src: main-two-cvs-while.c */
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <pthread.h>
#include <sys/time.h>
#include <string.h>
#include "common.h"
#include "common_threads.h"
#include "pc-header.h"

// 这是用于生产者消费者的信号量
// empty 用于通知生产者复工
// fill 用于通知消费者消费
// m 锁
pthread_cond_t empty = PTHREAD_COND_INITIALIZER;
pthread_cond_t fill = PTHREAD_COND_INITIALIZER;
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;

#include "main-header.h"

// 填充函数
void do_fill(int value) {
// 确保在这个函数使用前buffer[fill_ptr]是空的
// 否则打印错误讯息
ensure(buffer[fill_ptr] == EMPTY, "error: tried to fill a non-empty buffer");
// 填充缓冲区特定位置
buffer[fill_ptr] = value;
// 填充指针指向下一位置
fill_ptr = (fill_ptr + 1) % max;
// 已经填充数量加一
num_full++;
}

// 获取函数
int do_get() {
// 获取特定位置的值
int tmp = buffer[use_ptr];
// 确保缓冲区有值
// 否则打印错误讯息
ensure(tmp != EMPTY, "error: tried to get an empty buffer");
// 取完商品把这个位置置空
buffer[use_ptr] = EMPTY;
// 使用指针指向下一位置
use_ptr = (use_ptr + 1) % max;
// 已经填充数量减一
num_full--;
return tmp;
}

// 生产者函数
void *producer(void *arg) {
// 根据arg所指的数值设置生产者的id
int id = (int) arg;
// 确保每一个生产者能够生产自己专属的值(与id关联)
int base = id * loops;
int i;
for (i = 0; i < loops; i++) { p0; // 循环生产商品
Mutex_lock(&m); p1; // 获取锁
while (num_full == max) { p2; // 如果货架满了
Cond_wait(&empty, &m); p3; // 则睡眠等待货架再次为空时复工
}
do_fill(base + i); p4; // 进行一次货架填充
Cond_signal(&fill); p5; // 通知消费者可消费
Mutex_unlock(&m); p6; // 释放锁
}
return NULL;
}

void *consumer(void *arg) {
// 消费者根据arg产生唯一id
int id = (int) arg;
int tmp = 0;
int consumed_count = 0; // 消费数量
while (tmp != END_OF_STREAM) { c0; // 循环直到生产商品流的最后元素
Mutex_lock(&m); c1; // 获取锁
while (num_full == 0) { c2; // 如果货架空了
Cond_wait(&fill, &m); c3; // 等待生产者填充货架
}
tmp = do_get(); c4; // 获取货物
Cond_signal(&empty); c5; //
Mutex_unlock(&m); c6; // 释放锁
consumed_count++; // 消费数量加一
}
// 返回consumer_count-1是因为END_OF_STREAM并不是有效商品
return (void *) (long long) (consumed_count - 1);
}

// 设置控制信号
pthread_cond_t *fill_cv = &fill;
pthread_cond_t *empty_cv = &empty;

// 所有代码使用这个文件里的东西
// 包括主函数等启动问题程序
#include "main-common.c"

对上面的代码我们做出了解析,如注释所示。我们的生产者会调用do_fill进行生产活动,一次只生产一个货物到空货架上。消费者会调用do_get获取货物,一次只获得一个商品。货架是一个数组,上面依次存取商品,由fill_ptr指明放入位置,由use_ptr知名从哪个位置取商品。

2.

image-20220516085555854

运行一个生产者一个消费者程序,然后让生产者生产一些特定的值。一开始我们只是使用一个长度大小只有1的buffer,然后慢慢增长它。当货架增大,程序代码会做和行为?你认为num_full会随着buffer的容量变大有什么不同(例如可以设成-l 100),当你改变消费者字符串,从默认的不睡眠到-C 0,0,0,0,0,0,1会发生什么?

这是一个项目程序,我们应该先编译他们:

1
sh> make

编译完成生成了几个可执行的程序,现在我们让buffer长度为1,然后仅仅看看一个生产者和一个消费者的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
puitar@ubuntu:~/Desktop/ostep-homework/threads-cv
$ ./main-two-cvs-while -p 1 -c1 -m 1 -v
NF P0 C0
0 [*--- ] c0
0 [*--- ] p0
0 [*--- ] c1
0 [*--- ] c2
0 [*--- ] p1
1 [* 0 ] p4
1 [* 0 ] p5
1 [* 0 ] p6
1 [* 0 ] c3
0 [*--- ] c4
0 [*--- ] c5
1 [*EOS ] [main: added end-of-stream marker]
1 [*EOS ] c6
1 [*EOS ] c0
1 [*EOS ] c1
0 [*--- ] c4
0 [*--- ] c5
0 [*--- ] c6

Consumer consumption:
C0 -> 1

上面的数码做了一件事:[]中放的是buffer的内容,再往左是buffer的full_num。然后是对应的生产者消费者执行的步骤。(具体请见上面c代码)首先消费者执行c0,然后是消费者执行p0。然后消费者执行c1获取锁。然后while判断货架为空准备进入等待并释放锁。然后生产者获取锁,while判断货架为空跳过等待进行生产,然后唤醒消费者并释放锁。然后消费者苏醒获取锁,进行消费。此时以达到生产线最后元素(EOS)。然后消费者退出循环并返回。最后我们可以看到消费者C0消费了一个商品。上面过程没有出现什么问题。

接下来我们增大buffer尺寸。比如加到10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
puitar@ubuntu:~/Desktop/ostep-homework/threads-cv
$ ./main-two-cvs-while -p 1 -c1 -m 10 -v
NF P0 C0
0 [*--- --- --- --- --- --- --- --- --- --- ] c0
0 [*--- --- --- --- --- --- --- --- --- --- ] p0
0 [*--- --- --- --- --- --- --- --- --- --- ] p1
1 [u 0 f--- --- --- --- --- --- --- --- --- ] p4
1 [u 0 f--- --- --- --- --- --- --- --- --- ] p5
1 [u 0 f--- --- --- --- --- --- --- --- --- ] p6
1 [u 0 f--- --- --- --- --- --- --- --- --- ] c1
0 [ --- *--- --- --- --- --- --- --- --- --- ] c4
0 [ --- *--- --- --- --- --- --- --- --- --- ] c5
1 [ --- uEOS f--- --- --- --- --- --- --- --- ] [main: added end-of-stream marker]
1 [ --- uEOS f--- --- --- --- --- --- --- --- ] c6
1 [ --- uEOS f--- --- --- --- --- --- --- --- ] c0
1 [ --- uEOS f--- --- --- --- --- --- --- --- ] c1
0 [ --- --- *--- --- --- --- --- --- --- --- ] c4
0 [ --- --- *--- --- --- --- --- --- --- --- ] c5
0 [ --- --- *--- --- --- --- --- --- --- --- ] c6

Consumer consumption:
C0 -> 1

可以看到-l不发生变化,实际上和前面差不多,都没有什么问题。

然后我们现在增加生产次数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
puitar@ubuntu:~/Desktop/ostep-homework/threads-cv$ ./main-two-cvs-while -p 1 -c1 -m 10 -l 100  -v
NF P0 C0
0 [*--- --- --- --- --- --- --- --- --- --- ] p0
0 [*--- --- --- --- --- --- --- --- --- --- ] c0
0 [*--- --- --- --- --- --- --- --- --- --- ] p1
1 [u 0 f--- --- --- --- --- --- --- --- --- ] p4
1 [u 0 f--- --- --- --- --- --- --- --- --- ] p5
1 [u 0 f--- --- --- --- --- --- --- --- --- ] p6
...
1 [f--- --- --- --- --- --- --- --- --- u 99 ] p5
1 [f--- --- --- --- --- --- --- --- --- u 99 ] p6
1 [f--- --- --- --- --- --- --- --- --- u 99 ] c1
0 [*--- --- --- --- --- --- --- --- --- --- ] c4
0 [*--- --- --- --- --- --- --- --- --- --- ] c5
1 [uEOS f--- --- --- --- --- --- --- --- --- ] [main: added end-of-stream marker]
1 [uEOS f--- --- --- --- --- --- --- --- --- ] c6
1 [uEOS f--- --- --- --- --- --- --- --- --- ] c0
1 [uEOS f--- --- --- --- --- --- --- --- --- ] c1
0 [ --- *--- --- --- --- --- --- --- --- --- ] c4
0 [ --- *--- --- --- --- --- --- --- --- --- ] c5
0 [ --- *--- --- --- --- --- --- --- --- --- ] c6

Consumer consumption:
C0 -> 100

可以看到消费者都能够正常获得商品。

现在改变消费者睡眠字符串

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
puitar@ubuntu:~/Desktop/ostep-homework/threads-cv$ ./main-two-cvs-while -p 1 -c1 -m 10 -l 100  -v
NF P0 C0
0 [*--- --- --- --- --- --- --- --- --- --- ] p0
0 [*--- --- --- --- --- --- --- --- --- --- ] c0
0 [*--- --- --- --- --- --- --- --- --- --- ] p1
1 [u 0 f--- --- --- --- --- --- --- --- --- ] p4
1 [u 0 f--- --- --- --- --- --- --- --- --- ] p5
1 [u 0 f--- --- --- --- --- --- --- --- --- ] p6
1 [u 0 f--- --- --- --- --- --- --- --- --- ] c1
0 [ --- *--- --- --- --- --- --- --- --- --- ] c4
0 [ --- *--- --- --- --- --- --- --- --- --- ] p0
0 [ --- *--- --- --- --- --- --- --- --- --- ] c5
0 [ --- *--- --- --- --- --- --- --- --- --- ] c6
0 [ --- *--- --- --- --- --- --- --- --- --- ] p1
0 [ --- *--- --- --- --- --- --- --- --- --- ] c0
1 [ --- u 1 f--- --- --- --- --- --- --- --- ] p4
1 [ --- u 1 f--- --- --- --- --- --- --- --- ] p5
...
0 [ --- --- --- --- --- *--- --- --- --- --- ] p1
1 [ --- --- --- --- --- u 25 f--- --- --- --- ] p4
1 [ --- --- --- --- --- u 25 f--- --- --- --- ] c0
1 [ --- --- --- --- --- u 25 f--- --- --- --- ] p5
1 [ --- --- --- --- --- u 25 f--- --- --- --- ] p6
1 [ --- --- --- --- --- u 25 f--- --- --- --- ] c1
1 [ --- --- --- --- --- u 25 f--- --- --- --- ] p0
0 [ --- --- --- --- --- --- *--- --- --- --- ] c4
0 [ --- --- --- --- --- --- *--- --- --- --- ] c5
0 [ --- --- --- --- --- --- *--- --- --- --- ] c6
0 [ --- --- --- --- --- --- *--- --- --- --- ] p1
1 [ --- --- --- --- --- --- u 26 f--- --- --- ] p4
1 [ --- --- --- --- --- --- u 26 f--- --- --- ] c0
1 [ --- --- --- --- --- --- u 26 f--- --- --- ] p5
1 [ --- --- --- --- --- --- u 26 f--- --- --- ] p6
1 [ --- --- --- --- --- --- u 26 f--- --- --- ] p0
1 [ --- --- --- --- --- --- u 26 f--- --- --- ] c1
0 [ --- --- --- --- --- --- --- *--- --- --- ] c4
0 [ --- --- --- --- --- --- --- *--- --- --- ] c5
0 [ --- --- --- --- --- --- --- *--- --- --- ] c6
0 [ --- --- --- --- --- --- --- *--- --- --- ] p1
0 [ --- --- --- --- --- --- --- *--- --- --- ] c0
1 [ --- --- --- --- --- --- --- u 27 f--- --- ] p4
1 [ --- --- --- --- --- --- --- u 27 f--- --- ] p5
1 [ --- --- --- --- --- --- --- u 27 f--- --- ] p6
1 [ --- --- --- --- --- --- --- u 27 f--- --- ] c1
1 [ --- --- --- --- --- --- --- u 27 f--- --- ] p0
0 [ --- --- --- --- --- --- --- --- *--- --- ] c4
...
9 [f--- u 1 2 3 4 5 6 7 8 9 ] p0
9 [f--- u 1 2 3 4 5 6 7 8 9 ] p1
10 [ 10 * 1 2 3 4 5 6 7 8 9 ] p4
10 [ 10 * 1 2 3 4 5 6 7 8 9 ] p5
10 [ 10 * 1 2 3 4 5 6 7 8 9 ] p6
10 [ 10 * 1 2 3 4 5 6 7 8 9 ] p0
10 [ 10 * 1 2 3 4 5 6 7 8 9 ] p1
10 [ 10 * 1 2 3 4 5 6 7 8 9 ] p2
10 [ 10 * 1 2 3 4 5 6 7 8 9 ] c0
10 [ 10 * 1 2 3 4 5 6 7 8 9 ] c1
9 [ 10 f--- u 2 3 4 5 6 7 8 9 ] c4
9 [ 10 f--- u 2 3 4 5 6 7 8 9 ] c5
9 [ 10 f--- u 2 3 4 5 6 7 8 9 ] c6
9 [ 10 f--- u 2 3 4 5 6 7 8 9 ] p3
...
1 [uEOS f--- --- --- --- --- --- --- --- --- ] c0
1 [uEOS f--- --- --- --- --- --- --- --- --- ] c1
0 [ --- *--- --- --- --- --- --- --- --- --- ] c4
0 [ --- *--- --- --- --- --- --- --- --- --- ] c5
0 [ --- *--- --- --- --- --- --- --- --- --- ] c6

Consumer consumption:
C0 -> 100

看到上面的代码,生产f总是在使用u之前。

同时消费者总是等到缓冲区满了才开始消费。然后消费一个就通知生产者生产直到生产完毕。

此时一切生产消费活动都处于正常状态。

4.

image-20220516100021157

如果只有一个生产者,三个消费者,只有一个单一入口的buffer,每个消费者在c3指令执行的时候入睡要一秒钟(-C 0,0,0,1,0,0,0)

我们输入指令看看情况发生了什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
puitar@ubuntu:~/Desktop/ostep-homework/threads-cv
$ ./main-two-cvs-while -p 1 -c 3 -m 1 -C 0,0,0,1,0,0,0:0,0,0,1,0,0,0:0,0,0,1,0,0,0 -l 10 -v -t
NF P0 C0 C1 C2
0 [*--- ] c0
0 [*--- ] p0
0 [*--- ] c0
0 [*--- ] c0
0 [*--- ] c1
0 [*--- ] c2
0 [*--- ] p1
1 [* 0 ] p4
1 [* 0 ] p5
1 [* 0 ] p6
1 [* 0 ] c1
...
Consumer consumption:
C0 -> 0
C1 -> 10
C2 -> 0

Total time: 13.07 seconds
1
2
3
4
5
6
7
...
Consumer consumption:
C0 -> 9
C1 -> 0
C2 -> 1

Total time: 12.08 seconds

不同次运行同样的命令,每次的运行时间不一致。但至少10s,因为消费者会取出10个数据。每取一个都会有1s的休眠。

可以看到上面的代码能够保证生产10个消费10个,但是存在消费者会被饿死的情况。

8.

image-20220516101427280

现在看看main-one-cv-while.c,现在我们要写一个睡眠字符串,在只有一个生产者和一个消费者和一个缓冲区只有1大小的情况下产生问题。

一个生产者一个消费者不会产生什么问题,因为通信的只有生产者和消费者。但是如果出现两个生产者或者两个消费者的时候,一个条件变量就会不清楚通知的是生产者还是消费者。

9.

image-20220516103344533

这个问题是课本257页表格表示的只有一个条件变量的问题。因为条件变量只有一个,所以所以可能存在一个消费者消费完试图唤醒生产者,但是不是唤醒生产者而是唤醒另外一个消费者,最终造成发出信号的消费者睡着,生产者没有醒过来,另外一个消费者醒过来发现没有东西消费,然后又睡着的情况。最终三个人都睡着了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 生产者
for (i = 0; i < loops; i++) { p0; // 循环生产商品
Mutex_lock(&m); p1; // 获取锁
while (num_full == max) { p2; // 如果货架满了
Cond_wait(&empty, &m); p3; // 则睡眠等待货架再次为空时复工
}
do_fill(base + i); p4; // 进行一次货架填充
Cond_signal(&fill); p5; // 通知消费者可消费
Mutex_unlock(&m); p6; // 释放锁
}
// 消费者
while (tmp != END_OF_STREAM) { c0; // 循环直到生产商品流的最后元素
Mutex_lock(&m); c1; // 获取锁
while (num_full == 0) { c2; // 如果货架空了
Cond_wait(&fill, &m); c3; // 等待生产者填充货架
}
tmp = do_get(); c4; // 获取货物
Cond_signal(&empty); c5; //
Mutex_unlock(&m); c6; // 释放锁

我们可以看到p3是进行苏醒的,我们让它睡久一点,就能够触发问题了。比如这里我们让他睡五秒。

得到结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
puitar@ubuntu:~/Desktop/ostep-homework/threads-cv
$ ./main-one-cv-while -m 1 -l 5 -c 2 -p 1 -P 0,0,0,10,0,0,0 -v -t
NF P0 C0 C1
0 [*--- ] c0
0 [*--- ] p0
0 [*--- ] c0
0 [*--- ] p1
1 [* 0 ] p4
...
0 [*--- ] c5
0 [*--- ] c6

Consumer consumption:
C0 -> 5
C1 -> 0

image-20220516105426143

可以看到上面蓝色框框的部分C0消费完,不是唤醒P0,而是唤醒C1。

但是由于它编写的调度器总会在所有程序睡眠后调用生产者进行生产。所以可以看到蓝色框框下面又出现了生产者进行生产。但是实际上如果它的调度程序不这么智慧的话,三个线程都会进入睡眠状态。

10.

image-20220516105828890

这个题目要我们运行main-two-cvs-if.c文件。这个文件对等待函数是用if,也就是说仅仅进行一次判定。要我们考虑一个生产者一个消费者的情况,然后增加消费者的数量。试图产生问题。

我们说一个生产者一个消费者是不会有任何问题的。

但是如果有一个消费者或者以上,那么问题就出现了。这个问题其实就是书本255页介绍的问题。就是说只是进行一次等待判断,但是在两个消费者的时候,可能一个消费者判断有货物可取,然后再在它取之前发生中断,然后另外一个消费者也看到了这个货物,然后它一口气取完了。然后前面一个消费者还蒙在鼓里,以为自己还有货物,实际上已经没有货物了。

所以我们在其中一个消费者的去货物之前让它睡久一点就好,可以看到到c4是去货物所以试着构造这样的字符串

1
-C 0,0,0,0,5,0,0:0,0,0,0,0,0,0

执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
puitar@ubuntu:~/Desktop/ostep-homework/threads-cv
$ ./main-two-cvs-if -m 1 -l 5 -c 2 -p 1 -C 0,0,0,0,5,0,0:0,0,0,0,0,0,0 -v -t
NF P0 C0 C1
0 [*--- ] c0
0 [*--- ] p0
0 [*--- ] c1
0 [*--- ] c0
0 [*--- ] c2
0 [*--- ] p1
1 [* 0 ] p4
1 [* 0 ] p5
1 [* 0 ] p6
1 [* 0 ] c1
1 [* 0 ] p0
0 [*--- ] c4
0 [*--- ] c5
0 [*--- ] c6
0 [*--- ] c3
0 [*--- ] c0
error: tried to get an empty buffer

我们很容易就出发了这个错误。问题就是消费者2消费的速度明显更快,然后在15行的地方很快消费了货物,但是消费者1在c3的地方苏醒过来,没有再次判断是不是“假唤醒”(课本260页提示,所谓假唤醒就是唤醒你,但是你但是你没有货物),然后它认为它是有货物取得,所以在20行本来应该是消费者1进行取货c4操作,但是没有货物引起报错。

11.

image-20220516112113947

我们最后考察一个这样的程序main-two-cvs-while-extra-unlock.c。考察它会发生什么样的问题。如果你在放一个货物或者取一个货物之前就释放锁会发生什么。你可依托这一点给出一个睡眠字符串吗?分析发生了什么不好的事情。

和前面的代码不同的地方就在于它的生产者和消费者函数,是在释放锁之后才取资源。

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
void do_fill(int value) {
// ensure empty before usage
ensure(buffer[fill_ptr] == EMPTY, "error: tried to fill a non-empty buffer");
buffer[fill_ptr] = value;
fill_ptr = (fill_ptr + 1) % max;
num_full++;
}

int do_get() {
int tmp = buffer[use_ptr];
ensure(tmp != EMPTY, "error: tried to get an empty buffer");
buffer[use_ptr] = EMPTY;
use_ptr = (use_ptr + 1) % max;
num_full--;
return tmp;
}

void *producer(void *arg) {
int id = (int) arg;
// make sure each producer produces unique values
int base = id * loops;
int i;
for (i = 0; i < loops; i++) { p0;
Mutex_lock(&m); p1;
while (num_full == max) { p2;
Cond_wait(&empty, &m); p3;
}
Mutex_unlock(&m);
do_fill(base + i); p4;
Mutex_lock(&m);
Cond_signal(&fill); p5;
Mutex_unlock(&m); p6;
}
return NULL;
}

void *consumer(void *arg) {
int id = (int) arg;
int tmp = 0;
int consumed_count = 0;
while (tmp != END_OF_STREAM) { c0;
Mutex_lock(&m); c1;
while (num_full == 0) { c2;
Cond_wait(&fill, &m); c3;
}
Mutex_unlock(&m);
tmp = do_get(); c4;
Mutex_lock(&m);
Cond_signal(&empty); c5;
Mutex_unlock(&m); c6;
consumed_count++;
}

// return consumer_count-1 because END_OF_STREAM does not count
return (void *) (long long) (consumed_count - 1);
}

我们的公共资源就是buffer,但是把do_filldo_get放在锁的外面,这个锁就不起作用了。

如果只有一个消费者和生产者由于条件变量和full_num的共同作用,能够保证生产时消费者在睡,消费时生产者在睡。但是,如果有多个生产者和多个消费者的情况下,由于只能保证一个生产者和一个消费者互斥。所以其他的就可以对公共资源进行改写。比如在一个消费者取货物之前可能另外一个生产者覆盖了这个货物。

image-20220516114048979

比如这里的2是被消费者0消费了,但是它很可能在c4消费之前,他消费的东西就被别的消费者覆盖了。

我们可以让c4执行慢一点,刚好让别的生产者抢占锁

1
-C 0,0,0,0,5,0,0:0,0,0,0,5,0,0

第31章

1.

image-20220517094824131

第一题要我们实现fork/join问题,要求自己写一个线程fork-join的程序,你要工作的是作业目录下的fork-join.c文件。同时在child添加sleep(1)试着确定他的正确性。

和课本266页一致,但是要注意几点:

  • child释放post信号量应该在孩子已经工作完的情况下
  • 创建完子线程之后,主线程等待子线程完毕

下面书写代码如下

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
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include "common_threads.h"

sem_t s;

void *child(void *arg) {
printf("child\n");
sleep(1); // 验证父线程是否等待子线程完成
// use semaphore here
Sem_post(&s)
return NULL;
}

int main(int argc, char *argv[]) {
pthread_t p;
printf("parent: begin\n");
// init semaphore here
Sem_init(&s, 0)
Pthread_create(&p, NULL, child, NULL);
// use semaphore here
Sem_wait(&s)
printf("parent: end\n");
return 0;
}

关于上述代码有几点说明:

  • 这里关于信号量的使用用的是宏定义函数
1
2
3
#define Sem_init(sem, value)                             assert(sem_init(sem, 0, value) == 0);
#define Sem_wait(sem) assert(sem_wait(sem) == 0);
#define Sem_post(sem) assert(sem_post(sem) == 0);
  • 运行编译时,需要加上-pthreadtag运行结果如下

image-20220517114856648

  • 当加入sleep(1)的时候同样运行正确,验证代码正确性。

2.

image-20220517115033332

现在看看rendezvous问题。这个问题表述如下:你有两个线程,每一个线程都将要进入到rendezvous点执行代码。每一部分都不会在其他线程进入这个代码的时候退出,而是等待。思考使用两个信号量实现这个过程。详见rendezvous.c

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
42
#include <stdio.h>
#include <unistd.h>
#include "common_threads.h"

// 如果编写的代码正确,每一个子线程会打印各自的before信息,然后再打印各自的after信息
// 加入sleep(1)测试程序正确性

sem_t s1, s2;
// 两个信号量用于分别管理child1和child2的等待和继续

void *child_1(void *arg) {
printf("child 1: before\n");
// what goes here?
Sem_wait(&s1)
printf("child 1: after\n");
Sem_post(&s2)
return NULL;
}

void *child_2(void *arg) {
sleep(1); // 先睡一会在工作
printf("child 2: before\n");
// what goes here?
Sem_post(&s1)
Sem_wait(&s2)
printf("child 2: after\n");
return NULL;
}

int main(int argc, char *argv[]) {
pthread_t p1, p2;
printf("parent: begin\n");
// init semaphores here
Sem_init(&s1, 0)
Sem_init(&s2, 0)
Pthread_create(&p1, NULL, child_1, NULL);
Pthread_create(&p2, NULL, child_2, NULL);
Pthread_join(p1, NULL);
Pthread_join(p2, NULL);
printf("parent: end\n");
return 0;
}

一点说明:这里把信号量作为条件变量使用,当条件变量发生变化,可以继续执行。可以看到如果child1先工作,它打印完它的before信息之后,会等待s1的讯息。然后child2输出它的before信息,然后它唤醒child1,然后自己进入睡眠等待child1打印它的after信息,然后child1打印完会唤醒child2child2打印after讯息。最终实现次序打印。

结果如下:

image-20220517121316512

4.

image-20220517123707241

这道题要求我们自己编写读写锁的详细代码。在这里我们不需要考虑饿死的问题。

代码如下

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "common_threads.h"

//
// Your code goes in the structure and functions below
//

typedef struct __rwlock_t {
sem_t lock; // 二值信号量,用作基础锁,保证对readers操作的原子性
sem_t writelock; // 写锁
int readers; // 读者的数量
} rwlock_t;


void rwlock_init(rwlock_t *rw) {
rw->readers = 0; // 读者的数量
Sem_init(&rw->lock, 1) // 基础锁初始化
Sem_init(&rw->writelock, 1) // 写锁初始化
}

void rwlock_acquire_readlock(rwlock_t *rw) {
Sem_wait(&rw->lock)
rw->readers++;
if (rw->readers == 1)
Sem_wait(&rw->writelock)
Sem_post(&rw->lock)
}

void rwlock_release_readlock(rwlock_t *rw) {
Sem_wait(&rw->lock)
rw->readers--;
if (rw->readers == 0)
Sem_post(&rw->writelock)
Sem_post(&rw->lock)
}

void rwlock_acquire_writelock(rwlock_t *rw) {
Sem_wait(&rw->writelock)
}

void rwlock_release_writelock(rwlock_t *rw) {
Sem_post(&rw->writelock)
}

//
// Don't change the code below (just use it!)
//

int loops;
int value = 0;

rwlock_t lock;

void *reader(void *arg) {
int i;
for (i = 0; i < loops; i++) {
rwlock_acquire_readlock(&lock);
sleep(1);
printf("read %d\n", value);
rwlock_release_readlock(&lock);
}
return NULL;
}

void *writer(void *arg) {
int i;
for (i = 0; i < loops; i++) {
rwlock_acquire_writelock(&lock);
value++;
printf("write %d\n", value);
rwlock_release_writelock(&lock);
}
return NULL;
}

int main(int argc, char *argv[]) {
// 获取参数
assert(argc == 4);
int num_readers = atoi(argv[1]);
int num_writers = atoi(argv[2]);
loops = atoi(argv[3]);
// 创建进程池
pthread_t pr[num_readers], pw[num_writers];
// 读写锁初始化
rwlock_init(&lock);
printf("begin\n");
// 创建所有读者和写着
int i;
for (i = 0; i < num_readers; i++)
Pthread_create(&pr[i], NULL, reader, NULL);
for (i = 0; i < num_writers; i++)
Pthread_create(&pw[i], NULL, writer, NULL);
// 主线程等待所有读者写者完成任务
for (i = 0; i < num_readers; i++)
Pthread_join(pr[i], NULL);
for (i = 0; i < num_writers; i++)
Pthread_join(pw[i], NULL);

printf("end: value %d\n", value);

return 0;
}

运行结果如下

image-20220517131624836

存在读者饿死写着的问题。看到上面的实验结果显示只有读者全部读完了才开始写。根据代码我们也可以看出来。第一个读者获取写锁,所以任何写者都不可以写,而在第一个读者获得写锁后,其他的读者是可以获得读锁的。一旦某一个读者抢到了写锁,其他的读者都会蜂拥而入(它们不受任何阻碍)然后执行读操作。而写者呢?它们很不幸,如果它们没有任何一个读者快抢到writelock,则它们不能够执行写,因为writelock还在第一个读者手上(writelock移交到最后一个读者手上了才释放),所以这些读者只能够等待几乎所有读者读完才能够进行写操作。同时还要保证在它们被最后一个读者唤醒之后没有其他读者和他们竞争,才能够安稳进行写操作。

5.

image-20220517133750012

现在我们要写一个没有上面描述到的饥饿问题的程序。实际上我们需要做到的是:如何能够在有写者等待的时候,避免有更多的读者进入并持有锁。

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "common_threads.h"

//
// Your code goes in the structure and functions below
//

typedef struct __rwlock_t {
sem_t lock; // 二值信号量,用作基础锁
// 保证对writers和readers操作的原子性
sem_t writelock; // 写锁
int readers; // 读者的数量
int writers;
} rwlock_t;


void rwlock_init(rwlock_t *rw) {
rw->readers = 0; // 读者的数量
rw->writers = 0; // 等待写的写者数量
Sem_init(&rw->lock, 1) // 基础锁初始化
Sem_init(&rw->writelock, 1) // 写锁初始化
}

void rwlock_acquire_readlock(rwlock_t *rw) {

Sem_wait(&rw->lock)
while (rw->writers > 0) // 判断是否有写者进行等待
; // spin // 自旋等待
rw->readers++;
if (rw->readers == 1)
Sem_wait(&rw->writelock)
Sem_post(&rw->lock)
}

void rwlock_release_readlock(rwlock_t *rw) {
Sem_wait(&rw->lock)
rw->readers--;
if (rw->readers == 0)
Sem_post(&rw->writelock)
Sem_post(&rw->lock)
}

void rwlock_acquire_writelock(rwlock_t *rw) {
// 写者申请写锁前先将等待数目加一
Sem_wait(&rw->lock)
rw->writers++;
Sem_post(&rw->lock)
// 申请写锁
Sem_wait(&rw->writelock)
// 写者获得写锁把等待数目减一
Sem_wait(&rw->lock)
rw->writers--;
Sem_post(&rw->lock)
}

void rwlock_release_writelock(rwlock_t *rw) {
Sem_post(&rw->writelock)
}

// 下面的代码和上面的文件没有什么区别

这种策略实际上是一种写者优先的策略。由于读写锁的策略是针对多数读者少数写者的情况。这里我采用简单的自旋技术,让读者在申请锁之前总需要先判断一下是否有写者等待,如果有读者自选等待。然后在rwlock_acquire_writelock的代码中对writers的改写需要保证原子性,所以需要上锁。但是我们需要把锁的范围缩小到上面的仅对加减writers的操作上锁解锁。从第51行到第53行可能发生中断,使得尽管写者已经释放了锁,但是等待数目没有来得及减。但是此时所有读者仍然在等待减完才能抢占CPU。这一点在这里不用担心。

最后我们看一种夸张的情况,每一个读者在抢占锁之前都睡眠一秒钟。结果显示能够让写者先进行写操作。说明我们解决了饥饿的问题。

image-20220517145756037

6.

image-20220517145842415

现在要用信号量实现没有饥饿问题的互斥量也就是锁。

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include "common_threads.h"

//
// Here, you have to write (almost) ALL the code. Oh no!
// How can you show that a thread does not starve
// when attempting to acquire this mutex you build?
//

typedef struct __ns_mutex_t {
int ticket;
int turn;
sem_t stic;
sem_t stur;
} ns_mutex_t;

void ns_mutex_init(ns_mutex_t *m) {
Sem_init(&m->stic, 1)
Sem_init(&m->stur, 1)
m->ticket = 0;
m->turn = 0;
}

void ns_mutex_acquire(ns_mutex_t *m) {
// FetchAndAdd(&m->stic)
Sem_wait(&m->stic)
int myturn = m->ticket++;
Sem_post(&m->stic)
// 自旋等待轮询到自己
while (m->turn != myturn)
; // spin
}

void ns_mutex_release(ns_mutex_t *m) {
// FetchAndAdd(&m->stur)
Sem_wait(&m->stur)
m->turn++;
Sem_post(&m->stur)
}

static int value = 0;
ns_mutex_t lock;

void *worker(void *arg) {
ns_mutex_acquire(&lock);
int tmp = value++;
ns_mutex_release(&lock);
printf("tid: %ld > %d\n", pthread_self(), tmp);
return NULL;
}



int main(int argc, char *argv[]) {
assert(argc == 2);
int num_workers = atoi(argv[1]);

pthread_t pool[num_workers];
ns_mutex_init(&lock);
printf("parent: begin\n");
for (int i = 0; i < num_workers; i++)
Pthread_create(&pool[i], NULL, worker, NULL);
for (int i = 0; i < num_workers; i++)
Pthread_join(pool[i], NULL);
printf("parent: end\n");
return 0;
}

对于上面的代码,我们需要的是公平性,我们可以通过学习Mellor-Crummey和Michael Scott提出来的ticket和turn的机制(课本228页)写出代码如上。主要是用了这种turn的轮询机制。通过锁的全局ticketturn,以及每个线程申请时的myturn来实现公平。课本上主要使用原语FetchAndAdd实现对lock->turnlock->ticket原子操作,这里我们用信号量生成的锁来实现这种原语。其实上面带有FetchAndAdd的注释的地方的代码相当于下面

1
2
3
4
5
#define FetchAndAdd(sema) {
Sem_wait(&m->stic) \
/* 某些操作 */ \
Sem_post(&m->stic) \
}

最终运行结果如下,我们可以清楚看到,每个线程都调度了,达到了相对的公平性。

image-20220519222752600