Unix 环境高级编程

linux 一些零散的基础知识(视频系统编程部分)

快捷命令:

1
2
3
4
5
6
7
8
9
10
11
history 查看历史命令
ctrl + p 在历史命令中向上滚动
ctrl + n 在历史命令中向下滚动
ctrl + b 光标向前移动
ctrl + f 光标向后移动
ctrl + a 光标移动行首
ctrl + e 光标移动行尾
ctrl + h 删除光标前
ctrl + d 删除光标后
ctrl + u 删除光标前所有
tap 智能提示

目录结构:

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
ls / 查看目录
/*
-a 显示隐藏文件 -l 显示详细信息 -la 组合使用
*/
cd /xxx 进入xx文件
pwd 查看当前路径
mkdir 创建目录
mkdir dir/dir1/dir2 -p 创建符合目录 ,就是一层套一层
rmdir 删除空目录
rm aa -r -r代表递归,删除非空目录 -i 提示
touch 创建文件
cp xxxx ttt 将xxxx拷贝到ttt文件,不存在就创建ttt文件
cp 目录时需要加 -r 递归拷贝 和 rm 类似
cat 查看文件
more 查看文件 空格翻一页 回车翻一行 q退出
less 比more好一点 但后面已vi为主
head 显示文件前十行 可以加参数 head -5
tail 反之
mv xxx ttt 重命名
ln -s xxx ttt 创建ttt的软链接为xxx 用绝对路径

//修改文件权限
//1.文字设定法
chmod [who] [+|-|=] [mode] 修改文件权限
/*
+ 添加权限 = 覆盖权限 - 减少权限
u 文件所有者 g 文件所属组 o 其他人 a 所有的人

chmod a=w xxx 将xxx所有人的权限覆盖为 w
*/
//2.数字设定法
chmod 777 xxx
/*
-:没有权限 r:4 w:2 x:1
如765 对应
7 rwx 文件所属者
6 rw 文件所属组
5 rx 其他人
*/

//修改所属组
chown xxx ttt 将ttt的所属者改为xxx
chown xxx:yyy ttt 将ttt的所属者和所属组改为 xxx yyy
chgrp 修改所属组
/*
注意需要root权限
*/

//按文件属性查找
find /home/itcast/ -name "hel?"
/*
问号代表一个符号 , * 代表多个
*/
//按大小
find 查找目录 -size +10k
//按类型
find 查找目录 -type d/f/b/c/s/p/l

//按文件内容查找
grep -r "查找内容" 查找路径

//获取网络接口信息
ficonfig

//查看服务器域名对应ip
Nslookup

image-20220928125002199

上图为 -l 详细显示的信息的解释图

常见文件

1
2
3
4
5
6
7
8
9
/bin 命令
/dev 设备文件
/etc 配置文件信息
/home 所有用户的目录
/lib 存的一些动态库
/media 自动挂载库
/mnt 手动挂载
/root 管理员目录
/usr 当前用户软件安装目录

文件或目录熟悉

1
2
3
4
wc
od
du
df

image-20220929093226636

安装和卸载

1
2
3
4
5
6
7
8
sudo apt-get install xxx 在线下载安装
sudo apt-get remove xxx 移除
sudo apt-get update 更新软件列表
sudo apt-get clean 清除所有软件包

//deb包安装
sudo dpkg -i xxx.deb
sudo dpkg -r xxx

压缩

1
2
//默认功能较少的两种
gzip bzip2

image-20220929211522449

1
2
3
//解压缩
tar jxvf 压缩包名字(解压到当前目录)
tar jxvf 压缩包名字 -C 压缩的目录

image-20220929212619740

zip压缩目录需要-r

管道

image-20220930143055851

用户管理

image-20221001143600239

三种服务器搭建

ftp服务器

image-20221001143903737

image-20221001144100899

nfs服务器 (共享文件夹)

image-20221001144305890

ssh服务器

image-20221001144736655

vim 操作

image-20221001151454701

image-20221001153419355

image-20221001161348299

分屏: vsp sp !

gcc

1.预处理 gcc -E 2.编译 gcc -S 3.汇编 gcc -c 4.链接

1
2
3
4
5
6
7
8
9
-o    生成目标文件
-I 指定头文件目录
-D 编译时定义宏
-Wall 更多警告信息
-c 只编译子程序
-E 生成预处理文件
-g 包含调试信息
-0n (n=1,2,3) 编译优化

静态库

1
2
3
4
5
6
7
8
9
10
11
1)命名规则
a) lib + 库名 + .a
b) libmytest.a
2)制作步骤
a) 生成对应的.o文件
b) 将生成的.o文件打包 ar rcs + 静态库名字(libmytest.a) + 生成的.o
3)发布和使用静态库
a) 发布静态库
b) 头文件
4)使用静态库的例子
gcc main.c lib/libMyCalc.a -o calc -Iinclude

image-20221002144945691

动态库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1)命名规则
a) lib + 库名 + .so
2)制作步骤
a) 生成与位置无关的代码(生成与位置无关的.o)
gcc -fPIC -c *.c -I../include //多-fPIC参数
b) 将.o打包成共享库(动态库)
gcc -shared -o libMyCalc.so *.o -Iinclude
3)发布和使用动态库
gcc main.c lib/libMyCalc.so -o calc -Iinclude
4)动态库链接不到的原因
链接器找不到动态库.
a)设置LD_LIBRARY_PATH 临时导入
b)将export 写入.bashrc文件中
c)更改动态链接器的配置文件
ldconfig用于vi完后,更新配置

image-20221002152423010

c)

image-20221002153741139

image-20221002153809834

GDB调试

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
1)启动gdb
a) start -- 执行一步
b) n -- next
c) s -- step
c) c -- continue 直接停到断点位置
2)查看代码
a) l -- list
b) l + 行号
c) l + 文件名 或 函数名
3)设置断点
a) 设置当前文件断点 : b ; b + 行号
b) 设置指定文件断点 : b filename 行号
c) 设置条件断点 : b 行号 if value == 19
d) 删除断 : delete + 断点编号
e) 获取编号 : info b
4)单步调试
a) 进入函数体内部 : s
b) 从函数体内部跳出 : finish
c) 退出当前循环 : u
5)查看变量的值
p
6)查看变量类型
ptype 变量名
7)设置变量的值
set var = xx
8)设置追踪变量
display
9)取消设置追踪变量
undisplay 编号 获取编号 : info display
10)退出gdb
quit


//进程GBD时 利用循环创建子进程的架构 利用条件断点来切入子进程调试或父进程调试
1) set follow-fork-mode child //在fork之后追踪子进程
2) set follow-fork-mode parent //追踪父进程

Makefile

image-20221002195517567

image-20221002195535550

image-20221002202536556

虚拟地址空间

本质上类似虚拟地址到实际内存地址的映射,方便将零散的空间,构成一个连续的整体,和deque的状态有点像

image-20221003112740912

stat lstat区别,穿透和不穿透

image-20221004110828607

程序和进程

CPU基本运作方式

image-20221004200102729

MMU

image-20221004200200806

image-20221004202340296

进程控制块 PCB

image-20221004203527280

进程状态

image-20221004203151277

环境变量函数

1
2
3
getenv
unsetenv
setenv

FORK

1
2
3
4
5
6
//循环创建子进程
for (int i=0; i<n; i++)
{
if (fork() == 0)
break;
}

进程共享 特点:读时共享,写时复制

image-20221005205933042

exec函数族

加载一个进程,替换当前进程的代码

1
2
3
4
5
6
execlp   - p - path 
execl - l - list
execv - v - argv[]
execvp
execve - e - enviroment
只有失败返回-1

wait

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
//回收进程,防止出现孤儿进程,僵尸进程情况
//孤儿进程: 父进程提前结束
//僵尸进程: 子进程结束没回收

//1)
wait(status) : 返回 成功:pid 失败:-1
status: 传出参数
1.阻塞等待子进程
2.回收子进程资源
3.获取子进程结束状态
利用宏获取状态:
1.if WIFEXITED(status) (真) // 获取正常退出状态
WEXITSTATUS(status);
2.if WIFSIGNALED(status)(真)//获取异常的
WTERMSIG(status);

//2)
waitpid
1)参1: pid >0 指定进程id回收
pid = -1 回收任意子进程(wait)
pid = 0 回收本组任意子进程
pid < -1 回收该进程组的任意资产基础
2)参2: status 同上
3)参3: 0: (等价于wait) 阻塞回收
WNOHANG: 非阻塞回收(轮询)
成功: pid 失败:-1 返回0值: 非阻塞回收时,子进程还没结束

IPC

  1. 管道:使用简单
  2. FIFO:非血缘关系间
  3. 信号:开销小
  4. 共享内存:非血缘关系间
  5. 本地套接字

管道

image-20221006200819169

获取管道缓冲区大小 : 函数:fpathconf2 参2: __PC_PIPE_BUF 命令 : ulimit -a

管道优劣: 优 : 实现简单 缺 : 单向通信 血缘关系only

FIFO

mkfifo函数和命令

image-20221006202009432

共享存储映射

image-20221006203233785

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
int var = 100;

int main() {

int* p ;
pid_t pid;

int fd;
fd = open("temp",O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
perror("open error");
exit(1);
}

//删除临时文件目录项, 使之具备被释放条件, 所有使用该文件的进程结束后就被删除
unlink("temp");
ftruncate(fd, 4);

p = (int*)mmap(NULL, 4, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (p == MAP_FAILED) {
perror("mmap error");
exit(1);
}

close(fd);
//映射区创建完毕

pid = fork();
if (pid == 0) {
//子进程
*p = 2000;
var = 1000;
printf("child , *p = %d, var = %d\n", *p, var);
} else {
//父进程
sleep(1);
printf("parent, *p = %d, var = %d\n", *p, var);
wait(NULL);

int ret = munmap(p, 4);
if (ret == -1) {
perror("munmap error");
exit(1);
}

}

return 0;
}

由上图可以看出temp只是作为一个临时的映射区, 进程结束就rm掉了,实际上并不需要这个文件,由此引出匿名映射

匿名映射 (加入额外宏参数即可,原来传入文件的参数换位-1)

1
2
3
p = (int*)mmap(NULL, 4, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0);
//该宏只在linux系统下可用, 其他类unix系统不可用, 解决方法 借助/dev/zero
//大小可以随意指定, 本质仍是虚拟内存

image-20221006224421349

strace

strace + 可执行文件, 追踪程序执行过去中使用的系统调用,就比如利用程序进行进程通信时,底层其实还是使用mmap实现的通信

小实验 利用fifo实现简单的本地聊天室

image-20221009212823313

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
//server.h
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>

#define SERVER_FIFO "/home/aurora/learning/Day1007/test3/SERVER_FIFO"

typedef struct client {
//客户端名字
char client_name[20];
//私有管道描述符
int fifo_clifd;
} CL;

//记录客户端连接的数量
int client_len = 0;

//利用数组存储客户端队列
CL client_deque[100];

typedef struct message_pack {
//消息编号
int message_num;
//消息发送方
char sender_name[20];
//消息接收方
char receiver_name[20];
//数据
char data[1024];
} MSP;

//公共管道
int ser_fifo;
//服务器启动标志
int start_flag = 0;

//初始化服务器
void init_server();
//接受客户端信息包
void receiver_pack();
//解析客户端的包
void parsing_pack(MSP *msp);
//处理客户端登录, 插入客户队列, 创建私有管道
void client_login(char *login_name);
//处理消息发送给对应的客户端
void message_send(MSP *pMsp);
//处理客户端退出, 移出客户队列, 关闭私有管道
void client_quit(char *quit_name);
//关闭服务器
void close_server();
//处理输入数据
void message_handle(char *pMes);

//server.c
#include "server.h"
#define BUFSIZE 1068

void init_server() {

//将STDIN_FILENO改为非阻塞
int flags = fcntl(STDIN_FILENO, F_GETFL);
flags |= O_NONBLOCK;
fcntl(STDIN_FILENO, F_SETFL, flags);

//非阻塞的方式打开管道
ser_fifo = open(SERVER_FIFO, O_RDONLY | O_NONBLOCK);
if (ser_fifo < 0) {
perror("SERVER OPEN:");
exit(1);
}
printf("服务器已启动\n");
start_flag = 1-start_flag;
}

void receiver_pack() {

char buf[BUFSIZE];
MSP *msp;
int len = read(ser_fifo, buf, sizeof(buf));

if (len > 0) {
msp = (MSP*)buf;
parsing_pack(msp);
}
}

void parsing_pack(MSP *msp) {

switch (msp->message_num) {
case 0:
client_login(msp->sender_name);
break;
case 1:
message_send(msp);
break;
case 2:
client_quit(msp->sender_name);
break;
}
}

void client_login(char *login_name) {

strcpy(client_deque[client_len].client_name, login_name);
char path[23] = "./";
strcat(path, login_name);

//确保创建的文件的权限为分配权限
umask(0);
mkfifo(path, 0777);
//将管道的文件描述符存入数组
client_deque[client_len].fifo_clifd = open(path, O_WRONLY);
char buf[] = "您和服务器的连接已经成功建立, 可以开始通讯了\n";
write(client_deque[client_len].fifo_clifd, buf, sizeof(buf));

//将管道创建为临时的, 设置管道文件符合删除条件, 即程序结束后自动清楚
unlink(path);

++client_len;
}

void message_send(MSP *pMsp) {

int i = 0;
char *buf = (void*)pMsp;

if (strlen(pMsp->receiver_name) != 0) {
//单发
for (i = 0; i < client_len; ++i) {
if (strcmp(pMsp->receiver_name, client_deque[i].client_name) == 0) {
write(client_deque[i].fifo_clifd, buf, BUFSIZE);
break;
}
}
} else {
//群发
for (i = 0; i< client_len; ++i) {
write(client_deque[i].fifo_clifd, buf, BUFSIZE);
}
}
}

void client_quit(char *quit_name) {

int i = 0;

for (i = 0; i<client_len; ++i) {
if (strcmp(quit_name, client_deque[i].client_name) == 0) {
//关闭对应的私有管道
close(client_deque[i].fifo_clifd);
client_deque[i].fifo_clifd = -1;
client_deque[i].client_name[0] = '\0';
break;
}
}
printf("%s已退出\n", quit_name);
}

void message_handle(char *pMes) {

if (strcmp(pMes, "quit") == 0) {
close_server();
}
}

void close_server() {

char buf[] = "服务器维护中, 请稍后登录.";
int i = 0;

for (i = 0; i < client_len; ++i) {
if (client_deque[i].fifo_clifd != -1) {
write(client_deque[i].fifo_clifd, buf, sizeof(buf));
close(client_deque[i].fifo_clifd);
}
}

close(ser_fifo);
start_flag = 1-start_flag;
printf("已关闭所有管道, 服务器安全退出\n");
}

int main() {

init_server();
char mes[1024];

while (start_flag) {
receiver_pack();
if (scanf("%s", mes) != EOF) {
message_handle(mes);
}
}
return 0;
}

//client.h
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define SERVER_FIFO "/home/aurora/learning/Day1007/test3/SERVER_FIFO"

//连接标志
int link_flag = 0;
//公共管道描述符
int ser_fifo;
//私有管道描述符
int cli_fifo;
//客户端名称
char client_name[20];

typedef struct message_pack {
//消息编号
int message_num;
//消息发送方
char sender_name[20];
//消息接收方
char receiver_name[20];
//数据
char data[1024];
} MSP;

//初始化客户端
void init_client();
//登录服务器
void login_server();
//处理用户输入的数据
void message_handle(char *pMes);
//向服务器发送消息
void send_ser_mes(int mes_num);
//向其他用户发送消息
void send_oth_mes(char *receiver, char *data);
//接收消息
void receiver_mes();
//关闭客户端
void close_client();

//client.c
#include "client.h"
#define BUFSIZE 1068

void init_client() {

login_server();

//设置连接标志
link_flag = 1-link_flag;
//将STDIN文件属性修改为非阻塞
int flags = fcntl(STDIN_FILENO, F_GETFL);
flags |= O_NONBLOCK;
fcntl(STDIN_FILENO, F_SETFL, flags);
}

void login_server() {

printf("请输入客户端名称:");
scanf("%s", client_name);

ser_fifo = open(SERVER_FIFO, O_WRONLY | O_NONBLOCK);
if (ser_fifo < 0) {
perror("open server fifo");
exit(1);
}

send_ser_mes(0);

char path[23] = "./";
strcat(path, client_name);

//测试管道是否创建成功,成功返回0
while (access(path, F_OK) != 0) ;
cli_fifo = open(path, O_RDONLY | O_NONBLOCK);
if (cli_fifo < 0) {
perror("open client fifo");
exit(1);
}
printf("私有管道创建成功\n");
}

void send_ser_mes(int mes_num) {

MSP msp;
char * buf;
msp.message_num = mes_num;
strcpy(msp.sender_name, client_name);
buf = (void*)&msp;
write(ser_fifo, buf, sizeof(msp));
}

void message_handle(char *pMes) {

if (strcmp(pMes, "quit") == 0) {
send_ser_mes(2);
close_client();
}

//发送数据格式为 接收者姓名:消息内容
//如果不符合格式 消息改为群发

int i = 0, j = 0;
char receiver_name[20];
char data[1024];

while (pMes[i] != '\0' && pMes[i] != ':') {
receiver_name[i] = pMes[i];
++i;
}

receiver_name[i] = '\0';

if (pMes[i] == ':') {
++i;
} else {
i = 0;
receiver_name[0] = '\0';
}

while (pMes[i] != '\0') {
data[j++] = pMes[i++];
}
data[j] = '\0';

send_oth_mes(receiver_name, data);
}

void close_client() {

link_flag = 1-link_flag;
//关闭管道
close(cli_fifo);
close(ser_fifo);
printf("已关闭所有管道, 客户端端退出\n");
}

void receiver_mes() {

char buf[BUFSIZE];
int len = read(cli_fifo, buf, sizeof(MSP));
MSP * pMes = NULL;
pMes = (void*)buf;

if (len > 0 && pMes->message_num == 1) {
printf("%s:%s\n", pMes->sender_name, pMes->data);
} else if (len > 0) {
printf("系统提示:%s\n", buf);
}
}

void send_oth_mes(char *receiver, char *data) {

MSP msp;
char *buf;
msp.message_num = 1;
strcpy(msp.sender_name, client_name);
strcpy(msp.receiver_name, receiver);
strcpy(msp.data, data);
buf = (void*)&msp;
write(ser_fifo, buf, sizeof(msp));
}

int main () {

init_client();
char mes_buf[1024];

while (link_flag) {

if (scanf("%s", mes_buf) != EOF) {
message_handle(mes_buf);
}

receiver_mes();
}

return 0;
}

信号

内核发送, 内核处理

发送 ——- 过程 ——— 抵达

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
阻塞信号集合 使信号无法抵达,被阻塞
未决信号集合 由阻塞信号集影响

//信号产生
1.终端按键产生信号(ctrl +c/z/\)
2.硬件异常产生信号(除0操作,非法内存访问,总线错误)
3.kill函数/命令产生信号( int kill(pid_t pid, int sig); )
pid > 0 : 发送给指定进程
pid = 0 : 发送给kill(函数)同进程组进程
pid < 0 : 取|pid|发给对应进程组
pid = -1: 发送给有权发送的所有进程
4.int raise(int sig) 和 void abort(void)
raise发送给自己, abort发送自己异常信号
5.软件条件产生信号(alarm函数/setitimer)
每个进程有且只有唯一一个定时器
//setitimer

image-20221012153510031

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//信号集操作函数
sigset_t set; //信号集合的类型, 以位来进行操作的
int sigemptyset(sigset_t *set); //将信号集清0
int sigfillset(sigset_t *set); //将信号集置1
int sidaddset(sigset_t *set, int signum); //将某信号加入集合
int sigdeleset(sigset_T *set,int signum); //将某信号清出集合
int sigismemeber(const sigset_t *set,int signum); //判断某信号是否在信号集合中, 1在, 0不在, -1出错

//sigprocmask 用来屏蔽信号, 解除屏蔽 本质:读取修改进程的信号屏蔽字(pcb中)
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
set:传入参数 oldset:传出参数
how:1.SIG_BLOCK: 此时set表示需要屏蔽的信号 mask = mask | set
2.SIG_UNBLOCK: 此时set表示需要解除屏蔽的信号 mask = mask & ~set
3.SIG_SETMAKS: 用set直接覆盖原有mask mask = set

//sigpending 读取当前进程的未决信号集
int sigpending(sigset_t *set); set传出参数 成功:0 失败:-1 设置error

信号捕捉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//signal 注册一个信号捕捉函数 优点简洁, 但不统一, 避免使用

//sigaction
int sigaction(int signum, const struct sigaction* act, struct sigaction* oldact); act传入参数, oldact传出参数
//struct sigaction的结构
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int,siginfo_t*,void*);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
//各成员详细
1.sa_restorer 已废除
2.sigaction 当sa_flags指定为SA_SIGINFO时使用该函数,另外实际上的结构和sa_handler组成union
3.sa_handler 信号捕捉后的处理函数名 赋值为SIG_IGN表忽略,SIG_DFL默认
4.sa_mask 信号屏蔽集合, 表示调用处理函数时, 屏蔽的信号
5.sa_flags 通常为0, 默认属性

//信号捕捉的特性
1.进程正常运行时,默认PCB中有一个信号屏蔽字,假定为☆,它决定了进程自动屏蔽哪些信号。当注册了某个信号捕捉函数,捕捉到该信号以后,要调用该函数。而该函数有可能执行很长时间,在这期间所屏蔽的信号不由☆来指定。而是用sa_mask来指定。调用完信号处理函数,再恢复为☆。
2.XXX信号捕捉函数执行期间,XXX信号自动被屏蔽。
3.阻塞的常规信号不支持排队,产生多次只记录一次。(后32个实时信号支持排队)

内核实现信号捕捉过程:

**1.**在执行主控制流程时,由于中断,异常,系统调用(user)进入内核区 **-> 2.**内核处理完异常准备回用户区之前,处理当前进程中的可抵达信号(kernel) **-> 3.**如果设置了处理函数回到用户区,执行处理函数,但不是回到主控流程(kernel) **-> 4.**执行信号处理函数,然后调用特殊的系统调用sigretum再次进入内核区(user) **-> 5.**sys_sigreturn() 然后返回用户区,之前终端的主控流程(user)

image-20221012160530748

时序竞态

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
//1.示例
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>

void catch_sigalrm(int signo) {
;
}

unsigned int mysleep(unsigned int seconds) {

int ret;
struct sigaction act, oldact;

act.sa_handler = catch_sigalrm;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;

ret = sigaction(SIGALRM, &act, &oldact);
if (ret == -1) {
perror("sigaction error");
exit(1);
}

alarm(seconds);
//屏蔽

ret = pause();
if (ret == -1 && errno == EINTR) {
printf("pause sucess\n");
}

ret = alarm(0);
sigaction(SIGALRM, &oldact, NULL);

return ret;
}

int main() {
while (1) {
printf("--------------\n");
mysleep(3);
}

return 0;
}
//此处的问题在于alarm和pause之间,如果此时cpu的调度切出去了,在此期间alarm信号发送完了,并且抵达了,pause由于收不到信号,被永久阻塞, 首先想到的解决思路, 对alarm信号设置屏蔽,在pause之前再解除, 但无论怎么解决, 屏蔽和pause之间同样可能cpu切出去,导致信号已经处理完了, pause收不到信号永久阻塞,解决方法采用系统函数,由于系统函数是原子操作,pause的功能和解除屏蔽同时发生,不可再分,不会出现cpu突然抢走调度的情况

//2.解决示例
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>


void catch_sigalrm(int signo) {
;
}

unsigned int mysleep(unsigned int seconds) {

int ret;
struct sigaction newact, oldact;
sigset_t newmask, oldmask, suspmask;
unsigned int unslept;

//1.为SIGALRM设置捕捉函数
newact.sa_handler = catch_sigalrm;
sigemptyset(&newact.sa_mask);
newact.sa_flags = 0;
sigaction(SIGALRM, &newact, &oldact);

//2.设置阻塞信号集, 阻塞SIGALRM信号
sigemptyset(&newmask);
sigaddset(&newmask, SIGALRM);
sigprocmask(SIG_BLOCK, &newmask, &oldmask);

//3.定时n秒
alarm(seconds);

//4.构造一个临时有效的阻塞信号集
//在临时阻塞信号集里解除SIGALRM的阻塞
suspmask = oldmask;
sigdelset(&suspmask, SIGALRM);

//5.调用sigsupspend 系统函数原子操作, 将pause的阻塞和解除屏蔽同时进行
//不会发生解除屏蔽和阻塞操作直接, cpu切出去
sigsuspend(&suspmask);

unslept = alarm(0);

sigaction(SIGALRM, &oldact, NULL);

sigprocmask(SIG_SETMASK, &oldmask, NULL);

return unslept;

}

int main() {
while (1) {
printf("--------------\n");
mysleep(3);
}

return 0;
}

竞态条件,跟系统负载有很紧密的关系,体现出信号的不可靠性。系统负载越严重,信号不可靠性越强。

不可靠由其实现原理所致。信号是通过软件方式实现(跟内核调度高度依赖,延时性强),每次系统调用结束后,或中断处理处理结束后,需通过扫描PCB中的未决信号集,来判断是否应处理某个信号。当系统负载过重时,会出现时序混乱。

​ 这种意外情况只能在编写程序过程中,提早预见,主动规避,而无法通过gdb程序调试等其他手段弥补。且由于该错误不具规律性,后期捕捉和重现十分困难。

全局变量异步I/O

和时序竞态有点类似,

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
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>

int n = 0, flag = 0;
void sys_err(char *str)
{
perror(str);
exit(1);
}
void do_sig_child(int num)
{
printf("I am child %d\t%d\n", getpid(), n);
n += 2;
flag = 1;
sleep(1);
}
void do_sig_parent(int num)
{
printf("I am parent %d\t%d\n", getpid(), n);
n += 2;
flag = 1;
sleep(1);
}
int main(void)
{
pid_t pid;
struct sigaction act;

if ((pid = fork()) < 0)
sys_err("fork");
else if (pid > 0) {
n = 1;
sleep(1);
act.sa_handler = do_sig_parent;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGUSR2, &act, NULL); //注册自己的信号捕捉函数 父使用SIGUSR2信号
do_sig_parent(0);
while (1) {
/* wait for signal */;
if (flag == 1) { //父进程数数完成
kill(pid, SIGUSR1);
flag = 0; //标志已经给子进程发送完信号
}
}
} else if (pid == 0) {
n = 2;
act.sa_handler = do_sig_child;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGUSR1, &act, NULL);

while (1) {
/* waiting for a signal */;
if (flag == 1) {
kill(getppid(), SIGUSR2);
flag = 0;
}
}
}
return 0;
}

//示例中,通过flag变量标记程序实行进度。flag置1表示数数完成。flag置0表示给对方发送信号完成。问题出现的位置,在父子进程kill函数之后需要紧接着调用 flag,将其置0,标记信号已经发送。但,在这期间很有可能被kernel调度,失去执行权利,而对方获取了执行时间,通过发送信号回调捕捉函数,从而修改了全局的flag。如何解决该问题呢?可以使用后续课程讲到的“锁”机制。当操作全局变量的时候,通过加锁、解锁来解决该问题。现阶段,我们在编程期间如若使用全局变量,应在主观上注意全局变量的异步IO可能造成的问题

可重入函数/不可重入函数

1
2
3
4
5
6
7
8
1.	定义可重入函数,函数内不能含有全局变量及static变量,不能使用malloc、free
2. 信号捕捉函数应设计为可重入函数
3. 信号处理程序可以调用的可重入函数可参阅man 7 signal
4. 没有包含在上述列表中的函数大多是不可重入的,其原因为:
a) 使用静态数据结构
b) 调用了malloc或free
c) 是标准I/O函数

SIGCHILD信号

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
//产生条件
1.子进程终止时
2.子进程收到SIGSTOP信号停止时
3.子进程处于停止态,收到SIGCONT后唤醒时
//借助SIGCHILD信号回收子进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>

void sys_err(char *s) {
perror(s);
exit(1);
}

void do_sig_child(int signo) {
int status; pid_t pid;

while ((pid = waitpid(0, &status, WNOHANG)) > 0) {

if (WIFEXITED(status))
printf("child %d exit %d\n", pid, WEXITSTATUS(status));

else if (WIFSIGNALED(status))
printf("child %d cancel signal %d\n", pid, WTERMSIG(status));
}
}

int main() {

pid_t pid; int i;

for (i = 0; i < 10; i++) {
if ((pid = fork()) == 0)
break;
else if (pid < 0)
sys_err("fork");
}

if (pid == 0) {
int n = 1;

while (n--) {
printf("child ID %d\n", getpid());
sleep(1);
}
return i+1;
} else if (pid > 0) {
struct sigaction act;
act.sa_handler = do_sig_child;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGCHLD, &act, NULL);

while (1) {
printf("Parent ID %d\n", getpid());
sleep(1);
}
}
return 0;
}

//SIGCHLD信号注意问题
1.子进程继承了父进程的信号屏蔽字和信号处理动作,但子进程没有继承未决信号集
2.注意注册信号捕捉函数的位置
3.应该在fork之前, 阻塞SIGCHLD信号, 注册完捕捉函数后解除阻塞

信号传参

1
2
3
4
5
6
7
8
9
10
11
12
13
//1.发送信号传参
//sigqueue函数对应kill函数,但可以携带参数
int sigqueue(pid_t pid, int sig, const union sigval value);
union sigval {
int sival_int;
void *sival_ptr;
};
//注意传地址时,不同进程之间虚拟地址空间独立,将当前进程地址传递给另一进程没有意义

//2.捕捉信号传参
//sigaction
就是上述sigaction 中与sa_handler组成union的另一成员
void (*sa_sigaction)(int , siginfo_t*.void*);

中断系统调用

1
2
//主要分为慢速系统调用和其他系统调用
//慢速即可能会造成进程永久阻塞的哪一类

image-20221013000057549

终端

1
2
3
4
5
6
7
终端即输入输出设备的总称

//一个linxu系统启动的大致步骤
init --> fork --> exec --> getty --> 用户输入帐号 --> login --> 输入密码 --> exec --> bash

//为什么组合键不会被读到,存在一个特殊处理,线路规程

image-20221013000454257

进程组

1
2
3
4
5
6
7
8
9
//进程组操作函数
//1.getpgrp
pid_t getpgrp(void); //返回调用者的进程组id
//2.getpgid
pid_t getpgid(pid_t pid); //获取指定进程的进程组id
成功:0 失败:-1,设置errno
//3.setpgid
int setpgid(pid_t pid, pid_tgpid) //改变进程默认所属的进程组
非root进程只能改变自己创建的子进程,或有权操作的进程

会话

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//会话主要用来创建守护进程
//创建会话, 6点注意事项
1.调用进程不能是进程组组长,该进程变成新会话首进程(session header)
2.该进程成为一个新进程组的组长进程。
3.需有root权限(ubuntu不需要)
4.新会话丢弃原有的控制终端,该会话没有控制终端
5.该调用进程是组长进程,则出错返回
6.建立新会话时,先调用fork, 父进程终止,子进程调用setsid

//getsid 获取进程所属会话ID
pid_t getisid(pid_t pid)
成功:返回调用进程的会话ID;失败:-1,设置errno

//setsid 创建一个会话,并以自己的ID设置进程组ID,同时也是新会话的ID
pid_t setsid(void);
成功:返回调用进程的会话ID;失败:-1,设置errno

守护进程

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
//模型
1.创建子进程,父进程退出
//所有工作在子进程中进行形式上脱离了控制终端
2.在子进程中创建新会话
   setsid()函数
   //使子进程完全独立出来,脱离控制
3.改变当前目录为根目录
   chdir()函数
//防止占用可卸载的文件系统
//也可以换成其它路径
4.重设文件权限掩码
   umask()函数
//防止继承的文件创建屏蔽字拒绝某些权限
//增加守护进程灵活性
5.关闭文件描述符
   //继承的打开文件不会用到,浪费系统资源,无法卸载
6.开始执行守护进程核心工作
7.守护进程退出处理程序模型

//示例
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

void mydaemond() {

pid_t pid, sid;
int ret;

pid = fork();
if (pid > 0) {
exit(1);
}

sid = setsid();

ret = chdir("/home/aurora/");
if (ret == -1) {
perror("chdir error");
exit(1);
}

umask(0022);

close(STDIN_FILENO);
open("/dev/null", O_RDWR);
dup2(0, STDOUT_FILENO);
dup2(0, STDERR_FILENO);
}

int main() {

mydaemond();

while (1) {

}

return 0;
}

线程

1
2
3
4
5
6
//Linux 中线程是后期加入的, 过度的不是很完善, 和windos的实现可能有区别, 本质仍是进程
//LWP : light weight process 轻量级的进程(本质)
//进程: 独立地址空间,拥有PCB
//线程: 也有PCB, 但没有独立的地址空间(共享)
//区别:是否共享地址空间
//Linux下: 线程是最小的执行单位, 进程最小的分配资源单位,可以看作一个线程的进程

Linxu内核线程的实现原理

1
2
3
4
5
6
7
1. 轻量级进程(light-weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone
2. 从内核里看进程和线程是一样的,都有各自不同的PCB,但是PCB中指向内存资源的三级页表是相同的
3. 进程可以蜕变成线程
4. 线程可看做寄存器和栈的集合
5. 在linux下,线程最是小的执行单位;进程是最小的分配资源单位
//察看LWP号:ps –Lf pid 查看指定线程的lwp号。
//三级映射:进程PCB --> 页目录(可看成数组,首地址位于PCB中) --> 页表 --> 物理页面 --> 内存单元

线程共享/非共享资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//共享
1.文件描述符表
2.每种信号的处理方式
3.当前工作目录
4.用户ID和组ID
5.内存地址空间(.text/.data/.bss/heap/共享库)

//非共享
1.线程ID
2.处理器现场和栈指针(内核栈)
3.独立的栈空间(用户空间栈)
4.errno变量
5.信号屏蔽字
6.调度优先级

//线程优,缺点
优点: 1.提高程序并发性 2.开销小 3.数据通信,共享数据方便
缺点: 1.库函数不稳定 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
//1.pthread_self  对应进程 getpid()
pthread_t pthread_self(void); //alaways success

//2.pthread_create 对应进程 fork()
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void*(*start_routine)(void*), void* arg);
//返回值: 0成功, 失败错误号
1.参数一:传出参数,传出分配的线程ID
2.参数二:通常传NULL,表示线程使用默认属性, 想用具体属性可以修改该参数
3.参数三:函数指针,指向线程的主函数,函数运行结束,即线程结束
4.线程主函数执行期间,使用的参数

//3.pthread_exit 将单个线程退出
void pthread_exit(void* retval); //参数: 表示线程退出状态, 通常传NULL

//4.ptherad_join 对应进程 waitpid()
int pthread_join(pthread_t thread, void **retval);
对比
进程: main返回值, exit参数->int, 等待子进程结束 wait函数参数->int*
线程: 线程主函数返回值, pthread_exit->void*, 等待线程结束 pthread_join 参数->void**

//5.pthread_detach 实现线程分离 线程结束后自动释放, 不会产生僵尸进程
int pthread_detach(pthread_t thread);

//6.pthread_cancel 对应进程 kill()
int pthread_cancel(pthread_t thread);
"注意":线程的取消并不是实时的,而有一定的延时。需要等待线程到达某个取消点(检查点)。
类似于玩游戏存档,必须到达指定的场所(存档点,如:客栈、仓库、城里等)才能存储进度。杀死线程也不是立刻就能完成,必须要到达取消点。
取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open,pause,close,read,write..... 执行命令man 7 pthreads可以查看具备这些取消点的系统调用列表。也可参阅 APUE.12.7 取消选项小节。
可粗略认为一个系统调用(进入内核)即为一个取消点。如线程中没有取消点,可以通过调用pthreestcancel函数自行设置一个取消点。
被取消的线程, 退出值定义在Linux的pthread库中。常数PTHREAD_CANCELED的值是-1。可在头文件pthread.h中找到它的定义:#define PTHREAD_CANCELED ((void *) -1)。因此当我们对一个已经被取消的线程使用pthread_join回收时,得到的返回值为-1

//7.pthread_equal 比较两个线程id是否相等 可能未来线程id可能被修改为结构体实现
int pthread_equal(pthread_t t1, pthread_t t2);

//终止线程的方式
1.线程主函数return
2.调用pthread_cancel
3.调用pthread_exit

//控制原语对比
进程 线程
fork pthread_create
exit pthread_exit
wait pthread_join
kill pthread_cancel
getpid pthread_self

线程属性

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct
{
int etachstate; //线程的分离状态
int schedpolicy; //线程调度策略
struct sched_param schedparam; //线程的调度参数
int inheritsched; //线程的继承性
int scope; //线程的作用域
size_t guardsize; //线程栈末尾的警戒缓冲区大小
int stackaddr_set; //线程的栈设置
void* stackaddr; //线程栈的位置
size_t stacksize; //线程栈的大小
} pthread_attr_t;
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
//常用的
1.线程分离状态
2.线程栈大小
3.线程栈警戒缓冲区大小

"注意": 应先初始化线程属性, 再由pthread_create创建线程

//线程属性初始化
int pthread_attr_init(pthread_attr_t *attr);
//销毁线程属性
int pthread_attr_destroy(pthread_attr_t *attr);

//线程的分离状态
//设置线程属性:分离 or 非分离
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
//获取线程属性:分离 or 非分离
int pthread_attr_getdetachstate(pthread_attr_t *attr, int*detachstate);
//参数 attr: 已初始化线程
//detachstate: PTHREAD_CREATE_DETACHED(分离线程)
// PTHREAD_CREATE_JOINABLE(非分离线程)

//线程的栈地址
POSIX.1定义了两个常量_POSIX_THREAD_ATTR_STACKADDR 和_POSIX_THREAD_ATTR_STACKSIZE检测系统是否支持栈属性。也可以给sysconf函数传递_SC_THREAD_ATTR_STACKADDR或 _SC_THREAD_ATTR_STACKSIZE来进行检测。
当进程栈地址空间不够用时,指定新建线程使用由malloc分配的空间作为自己的栈空间。通过pthread_attr_setstack和pthread_attr_getstack两个函数分别设置和获取线程的栈地址。
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize); 成功:0;失败:错误号
int pthread_attr_getstack(pthread_attr_t *attr, void **stackaddr, size_t *stacksize); 成功:0;失败:错误号
参数: attr:指向一个线程属性的指针
stackaddr:返回获取的栈地址
stacksize:返回获取的栈大小

//线程的栈大小
线程的栈大小
当系统中有很多线程时,可能需要减小每个线程栈的默认大小,防止进程的地址空间不够用,当线程调用的函数会分配很大的局部变量或者函数调用层次很深时,可能需要增大线程栈的默认大小。
函数pthread_attr_getstacksize和 pthread_attr_setstacksize提供设置。
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize); 成功:0;失败:错误号
int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize); 成功:0;失败:错误号
参数: attr:指向一个线程属性的指针
stacksize:返回线程的堆栈大小

//示例代码
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

#define SIZE 0x100000

void* th_fun(void* arg) {
while (1) {
sleep(1);
}
}

int main() {
pthread_t tid;
int err, detachstate, i = 1;
pthread_attr_t attr;
size_t stacksize;
void * stackaddr;

pthread_attr_init(&attr);
pthread_attr_getstack(&attr, &stackaddr, &stacksize);
pthread_attr_getdetachstate(&attr,&detachstate);

if (detachstate == PTHREAD_CREATE_DETACHED)
printf("thread detached\n");
else if (detachstate == PTHREAD_CREATE_JOINABLE)
printf("thread join\n");
else
printf("thread unknown\n");

pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

while (1) {
stackaddr = malloc(SIZE);
if (stackaddr == NULL) {
perror("malloc");
exit(1);
}

stacksize = SIZE;
pthread_attr_setstack(&attr, stackaddr, stacksize);
err = pthread_create(&tid, &attr, th_fun, NULL);
if (err != 0) {
printf("%s\n", strerror(err));
exit(1);
}
printf("%d\n", i++);
}
pthread_attr_destroy(&attr);
return 0;
}

//线程使用注意事项
1.主线程推出其他线程不退出,主线程应调用pthread_exit
2.避免僵尸线程
pthread_join
pthread_detach

小实验 多线程拷贝并实现进度条

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
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/mman.h>

#define T_NUM 5
#define ITEMS 50

void err_sys(void *str)
{
perror(str);
exit(1);
}

void err_usr(char *str)
{
fputs(str, stderr);
exit(1);
}

typedef struct{
int off, size, t_no;
}arg_t;

char *s, *d;
int *done;
int n = T_NUM;

//arg{off, size, t_no;}
void *tfn(void *arg)
{
arg_t *arg_p; int i;
char *p, *q;

arg_p = (arg_t *)arg;
p = s + arg_p->off, q = d + arg_p->off;
for(i = 0; i < arg_p->size; i++)
{
/* 逗号表达式的使用技巧*/
*q++ = *p++, done[arg_p->t_no]++;
usleep(10);
}

return NULL;
}

void *display(void *arg)
{
int size, interval, draw, sum, i, j;

size = (int)arg;
interval = size / (ITEMS - 1);
draw = 0;
while(draw < ITEMS){
for(i = 0, sum = 0; i < n; i++)
sum += done[i];
j = sum / interval + 1;
for(; j > draw; draw++){
putchar('='); fflush(stdout);
}
}
putchar('\n');

return NULL;
}

int main(int argc, char *argv[])
{
int src, dst, i, len, off;
struct stat statbuf;
pthread_t *tid;
arg_t *arr;

if(argc != 3 && argc != 4)
err_usr("usage : cp src dst [thread_no]\n");
if(argc == 4)
n = atoi(argv[3]);

src = open(argv[1], O_RDONLY);
if(src == -1)
err_sys("fail to open");
dst = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0644);
if(dst == -1)
err_sys("fail to open");

if(fstat(src, &statbuf) == -1)
err_sys("fail to stat");

lseek(dst, statbuf.st_size - 1, SEEK_SET);
write(dst, "a", 1); //IO操作拓展文件大小,也可以使用truncate

s = (char *)mmap(NULL, statbuf.st_size, PROT_READ, MAP_PRIVATE, src, 0);
if(s == MAP_FAILED)
err_sys("fail to mmap");
d = (char *)mmap(NULL, statbuf.st_size, PROT_WRITE , MAP_SHARED, dst, 0);
if(d == MAP_FAILED)
err_sys("fail to mmap");

close(src); close(dst);
//pthread_t tid[n+1];
tid = (pthread_t *)malloc(sizeof(pthread_t) * (n + 1));
if(tid == NULL)
err_sys("fail to malloc");
//int done[n] 每个线程完成任务字节数
done = (int *)calloc(sizeof(int), n);
if(done == NULL)
err_sys("fail to malloc");
//arr[n] 每个线程的任务
arr = (arg_t *)malloc(sizeof(arg_t) * n);
if(arr == NULL)
err_sys("fail to malloc");

//构建线程任务数组,分配任务
len = statbuf.st_size / n, off = 0;
for(i = 0; i < n; i++, off += len)
arr[i].off = off, arr[i].size = len, arr[i].t_no = i;
arr[n - 1].size += (statbuf.st_size % n);

//创建执行拷贝任务线程
for(i = 0; i < n; i++)
pthread_create(&tid[i], NULL, tfn, (void *)&arr[i]);

//创建进度线程
pthread_create(&tid[n], NULL, display, (void *)statbuf.st_size);

for(i = 0; i < n + 1; i++)
pthread_join(tid[i], NULL);
#if 1
munmap(s, statbuf.st_size);
munmap(d, statbuf.st_size);
#endif
free(tid); free(done); free(arr);

return 0;
}

线程同步

大概意思就是, 多线程对一个共享资源操作是, 由于竞争关系, 导致数据混乱, 用于解决这个问题, 就是线程同步

互斥量 Mutex (初始值1)

互斥锁实质上是”建议锁”, 即使有了互斥锁, 如果有线程不按规则访问, 仍然数据混乱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//pthread_mutex_init()
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
//restrict关键字:只用于限制指针,告诉编译器,所有修改该指针指向内存中内容的操作,只能通过本指针完成。不能通过除本指针以外的其他变量或指针修改
1:&mutex 参2:互斥量属性 通常传null
静态初始化: pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

//pthread_mutex_destroy()
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//pthread_mutex_lock()
int pthread_mutex_lock(pthread_mutex_t *mutex);
//pthread_mutex_trylock()
int pthread_mutex_trylock(pthread_mutex_t *mutex);
//pthread_mutex_unlock()
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//pthread_mutex_t类型


加锁与解锁

lock 和 unlock :

lock尝试加锁, 如果失败, 线程阻塞, 阻塞到该互斥量的其他线程解锁为止

unlock主动解锁, 同时将阻塞再该锁上的所有线程唤醒

lock 和 trylock :

lock加锁失败会阻塞, 等待锁释放

trylock加锁失败直接返回错误号, 不阻塞

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
//mutex使用例子
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>

pthread_mutex_t mutex;

void* tfn(void* arg) {
srand(time(NULL));

while (1) {

pthread_mutex_lock(&mutex);
printf("hello ");
sleep(rand() % 3);
printf("world\n");
pthread_mutex_unlock(&mutex);
sleep(rand() % 3);
}

return NULL;
}

int main() {
pthread_t tid;
srand(time(NULL));

pthread_mutex_init(&mutex, NULL);

pthread_create(&tid, NULL, tfn, NULL);

while (1) {
pthread_mutex_lock(&mutex);
printf("HELLO ");
sleep(rand() % 3);
printf("WORLD\n");
pthread_mutex_unlock(&mutex);
sleep(rand() % 3);
}

pthread_mutex_destroy(&mutex);

return 0;
}

死锁

锁的不正确使用造成线程永久阻塞

1.线程试图对同一个互斥量A加锁两次

2.线程1拥有A锁, 请求获得B锁, 线程2拥有B锁, 请求获得A锁

第二种解决: 两线程按同一顺序获取锁; 或者第二把锁请求失败时, 主动解锁自己掌握的锁

读写锁

读写锁一般具有三种状态:写锁, 读锁, 不加锁

1
2
3
4
1.读写锁是“写模式加锁”时, 解锁前,所有对该锁加锁的线程都会被阻塞。
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
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
//pthread_rwlock_init函数
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
2:读写锁属性, 默认Null
//pthread_rwlock_destroy函数
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
//pthread_rwlock_rdlock函数
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
读锁lock
//pthread_rwlock_wrlock函数
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
写锁lock
//pthread_rwlock_tryrdlock函数
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
非阻塞读锁lock
//pthread_rwlock_trywrlock函数
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
非阻塞写锁lock
//pthread_rwlock_unlock函数
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
//pthread_rwlock_t 类型

//读写锁使用例子
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

int counter;
pthread_rwlock_t rwlock;

void *th_write(void *arg) {
int t;
int i = (int)arg;

while (1) {
t = counter;
usleep(1000);

pthread_rwlock_wrlock(&rwlock);
printf("=======write %d: %lu: counter=%d ++ counter= %d\n",i, pthread_self(), t, ++counter);
pthread_rwlock_unlock(&rwlock);

usleep(5000);
}

return NULL;
}

void *th_read(void* arg) {
int i = (int)arg;

while (1) {
pthread_rwlock_rdlock(&rwlock);
printf("======read %d: %lu: %d\n",i ,pthread_self(), counter);
pthread_rwlock_unlock(&rwlock);

usleep(900);
}

return NULL;
}

int main() {
int i;
pthread_t tid[8];

pthread_rwlock_init(&rwlock, NULL);

for (i = 0; i < 3; i++)
pthread_create(&tid[i], NULL, th_write, (void*)i);

for (i = 0; i < 5; i++)
pthread_create(&tid[i+3], NULL, th_read, (void*)i);

for (i = 0; i < 8; i++)
pthread_join(tid[i], NULL);

pthread_rwlock_destroy(&rwlock);

return 0;
}

条件变量

条件变量本身不是锁!但它也可以造成线程阻塞。通常与互斥锁配合使用。给多线程提供一个会合的场所

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
//pthread_cond_init函数
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
2:条件变量属性, 通常传NULL
静态初始化: pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
//pthread_cond_destroy函数
int pthread_cond_destroy(pthread_cond_t *cond);
//pthread_cond_wait函数
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
函数作用:
1.阻塞等待条件变量cond满足
2.释放已掌握的互斥锁, 相当于pthread_mutex_unlock(&mutex);
1,2步为一个原子操作
3.当被唤醒, pthread_cond_wait返回时, 解除阻塞并重新申请互斥锁
//pthread_cond_timedwait函数
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
3: 参看man sem_timedwait函数,查看struct timespec结构体。
struct timespec {
time_t tv_sec; /* seconds */
long tv_nsec; /* nanosecondes*/ 纳秒
}
形参abstime:绝对时间。
如:time(NULL)返回的就是绝对时间。而alarm(1)是相对时间,相对当前时间定时1秒钟。
struct timespec t = {1, 0};
pthread_cond_timedwait (&cond, &mutex, &t); 只能定时到 19701100:00:01秒(早已经过去)
正确用法:
time_t cur = time(NULL); 获取当前时间。
struct timespec t; 定义timespec 结构体变量t
t.tv_sec = cur+1; 定时1
pthread_cond_timedwait (&cond, &mutex, &t); 传参 参APUE.11.6线程同步条件变量小节
在讲解setitimer函数时我们还提到另外一种时间类型:
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */ 微秒
};

//pthread_cond_signal函数
int pthread_cond_signal(pthread_cond_t *cond);
唤醒至少一个阻塞在条件变量上的线程
//pthread_cond_broadcast函数
int pthread_cond_broadcast(pthread_cond_t *cond);
唤醒所有
//pthread_cond_t 类型

//使用例子
//生产者-消费者模型
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <stdio.h>

/*链表作为共享数据,被mutex保护*/
struct msg {
struct msg *next;
int num;
};

struct msg *head;
struct msg *mp;


/*静态初始化 一个条件变量 一个互斥量*/
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

void *consumer(void* p) {

for (;;) {
pthread_mutex_lock(&lock);

while (head == NULL) {
pthread_cond_wait(&has_product, &lock);
}
mp = head;
head = mp->next;
pthread_mutex_unlock(&lock);

printf("-Consume ---%d\n", mp->num);
free(mp);
sleep(rand() % 5);
}
}

void *productor(void *p) {

for (;;) {
mp = malloc(sizeof(struct msg));
mp->num = rand() % 1000 + 1;
printf("-Produce ---%d\n", mp->num);

pthread_mutex_lock(&lock);
mp->next = head;
head = mp;
pthread_mutex_unlock(&lock);

pthread_cond_signal(&has_product);
sleep(rand() % 5);
}
}

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

pthread_t pid, cid;
srand(time(NULL));

pthread_create(&pid, NULL, productor, NULL);
pthread_create(&cid, NULL, consumer, NULL);

pthread_join(pid, NULL);
pthread_join(cid, NULL);

return 0;
}

信号量

互斥量初始值1, 到0 阻塞, 信号量初始值n, 到0阻塞

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
//信号量基本操作
sem_wait : 信号量大于0 ,信号量-- ; 信号量等于0 ,线程阻塞
类比pthread_mutex_lock
sem_post : 信号量++,同时唤醒所有阻塞的线程
类比pthread_mutex_unlock
信号量的初值决定了占用信号量线程的个数, 与mutex类似, sem_t 实现对用户隐藏, +--通过函数实现

//sem_init函数
int sem_init(sem_t *sem, int pshared, unsigned int value);
1:sem信号量
2:取0用于线程间, 取非0用于进程间
3:value指定信号量初值
//sem_destroy函数
int sem_destroy(sem_t *sem);
//sem_wait函数
int sem_wait(sem_t *sem);
加锁lock
//sem_trywait函数
int sem_trywait(sem_t *sem);
类比trylock
//sem_timedwait函数
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
2:abs_timeout采用的是绝对时间。
定时1秒:
time_t cur = time(NULL); 获取当前时间。
struct timespec t; 定义timespec 结构体变量t
t.tv_sec = cur+1; 定时1
t.tv_nsec = t.tv_sec +100;
sem_timedwait(&sem, &t); 传参

//sem_post函数
int sem_post(sem_t *sem);
解锁unlock
//sem_t 类型 头文件<semaphore.h>

//生产者消费者模型, 思路上会和前面的几种锁有一定不同, 前面都是对一个资源进行上锁,信号量虽然也是对一个资源,但是两把锁影响互相影响达到目的
/*信号量实现生产者消费者模型*/

#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <stdio.h>
#include <semaphore.h>

#define NUM 5

int queue[NUM];
sem_t blank_number , product_number;

void* producer(void* arg) {
int i = 0;

while (1) {
sem_wait(&blank_number);
queue[i] = rand() % 1000 + 1;
printf("----Produce---%d\n", queue[i]);
sem_post(&product_number);

i = (i + 1) % NUM;
sleep(rand() %1);
}
}

void* consumer(void *arg) {
int i = 0;

while (1) {
sem_wait(&product_number);
printf("-Consume---%d\n", queue[i]);
queue[i] = 0;
sem_post(&blank_number);

i = (i+1) % NUM;
sleep(rand() %3);
}
}

int main() {
pthread_t pid, cid;

sem_init(&blank_number, 0, NUM);
sem_init(&product_number, NULL, 0);

pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer, NULL);

pthread_join(pid, NULL);
pthread_join(cid, NULL);

sem_destroy(&blank_number);
sem_destroy(&product_number);

return 0;
}

进程同步

进程间同步也可以使用互斥锁,但应该在初始化之前将其属性修改为进程间共享

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
pthread_mutexattr_t mattr 类型:		用于定义mutex锁的【属性】
pthread_mutexattr_init函数: 初始化一个mutex属性对象
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
pthread_mutexattr_destroy函数: 销毁mutex属性对象 (而非销毁锁)
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
pthread_mutexattr_setpshared函数: 修改mutex属性。
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);
2:pshared取值:
线程锁:PTHREAD_PROCESS_PRIVATE (mutex的默认属性即为线程锁,进程间私有)
进程锁:PTHREAD_PROCESS_SHARED

//ps:进程间的全局变量, 读时共享, 写时复制, 所以应该创建一块映射区
//示例代码
#include <fcntl.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <stdio.h>

struct mt {
int num;
pthread_mutex_t mutex;
pthread_mutexattr_t mutexattr;
};

int main() {
int i;
struct mt *mm;
pid_t pid;

mm = mmap(NULL, sizeof(*mm), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0);
memset(mm, 0, sizeof(*mm));

pthread_mutexattr_init(&mm->mutexattr);
pthread_mutexattr_setpshared(&mm->mutexattr, PTHREAD_PROCESS_SHARED);

pthread_mutex_init(&mm->mutex, &mm->mutexattr);

pid = fork();

if (pid == 0) {
for (i = 0; i < 10; i++) {
pthread_mutex_lock(&mm->mutex);
(mm->num)++;
printf("-child-------------num++ %d\n", mm->num);
pthread_mutex_unlock(&mm->mutex);
sleep(1);
}
} else if (pid > 0) {
for (i = 0; i < 10; i++) {
sleep(1);
pthread_mutex_lock(&mm->mutex);
mm->num += 2;
printf("------parent------num+=2 %d\n", mm->num);
pthread_mutex_unlock(&mm->mutex);
}
wait(NULL);
}

pthread_mutexattr_destroy(&mm->mutexattr);
pthread_mutex_destroy(&mm->mutex);
munmap(mm, sizeof(*mm));

return 0;
}

文件锁

借助fcntl函数实现锁机制,操作文件的进程没有获得锁时,可以打开,但无法执行read,write

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
//效果上和线程同步的读写锁类似, 用于实现进程的间的读写锁

int fcntl(int fd, int cmd, .../*arg*/);
2:
F_SETLK(struct flock*) 设置文件锁(trylock)
F_SETLKW(struct flock*)设置文件锁(lock) W->wait
F_GETLK(struct flock*) 获取文件锁
参3:
struct flock {
...
short l_type; 锁的类型:F_RDLCK 、F_WRLCK 、F_UNLCK
short l_whence; 偏移位置:SEEK_SET、SEEK_CUR、SEEK_END
off_t l_start; 起始偏移:1000
off_t l_len; 长度:0表示整个文件加锁
pid_t l_pid; 持有该锁的进程ID:(F_GETLK only)
...
};

//使用例子
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>

void sys_err(char *str) {
perror(str);
exit(1);
}

int main(int argc, char* argv[]) {
int fd;
struct flock f_lock;

if (argc < 2) {
printf("./a.out filename\n");
exit(1);
}

if ((fd = open(argv[1], O_RDWR)) < 0)
sys_err("open");

f_lock.l_type = F_WRLCK; /*写锁*/

// f_lock.l_type = F_RDLCK; /*读锁*/

f_lock.l_whence = SEEK_SET;
f_lock.l_start = 0;
f_lock.l_len = 0;

fcntl(fd, F_SETLKW, &f_lock);
printf("get flock\n");
sleep(10);

f_lock.l_type = F_UNLCK;
fcntl(fd, F_SETLKW, &f_lock);
printf("un lock\n");

close(fd);
return 0;
}

APUE书籍部分笔记

重点章节:3,4,5,7,8,10,11,12 优先看

第3章 文件I/O

3.1 引言

本章描述的函数被称为 不带缓冲的I/O, 不带缓冲指的是每个read和write都调用内核中的一个系统调用

3.2 文件描述符

对于内核而言,所有打开的文件都通过文件描述符引用,0 ,1, 2 文件描述符本别于标准输入, 标准输出, 标准错误关联

3.3 函数open 和 openat

1
2
3
4
5
6
7
8
9
10
#include <fcntl.h>
int open(const *path, int oflag, ... /*mode_t mode*/);
int openat(int fd, const char *path, int oflag, ... /*mode_t mode*/);
//成功返回文件描述符, 失败返回-1

//path 打开或创建的文件名, oflag 参数, 此函数的多个选项
//下列5个常量中必须指定一个且只能指定一个
O_RDONLY 只读打开 O_WRONLY 只写打开 O_RDWR 读写打开 O_EXEC 只执行打开 O_SEARCH 只搜索打开
//下列常量是可选的
O_APPEND 每次写时追加到文件的尾端 O_CLOEXEC 把FD_CLOEXEC设置为文件描述符标志 O_CREATE 若文件不存在则创建它,使用此选项时需要指定mode_t参数 O_DIRECTORY 如果path引用的不是目录则出错 O_EXCL 如果同时指定了O_CREATE文件已经存在则出错,用此可以测试一个文件是否存在,不存在则创建此文件,测试和创建文件为一个原子操作 O_NOCTTY 如果path引用的是终端设备,则不将该设备分配为此进程的控制终端 O_NOFOLLOW 如果path引用的是一个符号链接则出错 O_NONBLOCK 设置非阻塞 O_SYNC 使每次write等待物理I/O操作完成 O_TRUNC 如果此文件存在,而且为只写或读写成功打开,将其长度截断为0 O_TTY_INIT 如果打开一个还未打开的终端设备,设置非标准termios参数值 O_DSYNC O_RSYNC

可以看到open和openat在参数上,openat多了一个fd

fd把open和openat区分开来,三种可能性:

1. path参数指定的是绝对路径名, 此时fd参数被忽略, openat相当于open
1. path参数指定的是相对路径名, fd参数指出相对路径名在文件系统中的开始地址, fd参数是通过打开相对路径名所在的目录来获取
1. path参数指定相对路径名, fd参数具有AT_FDCWD, 此时路径名在当前工作目录中获得, 也类似于open

目的:

  1. 让线程使用相对路径打开目录的文件, 同一进程中的所有线程共享相同的当前工作目录, 因此很难让同一进程的多个不同线程在同一时间工作在不同的目录
  2. 可以避免time-of-check-to-time-of-use问题

文件名和路径名截断

就是说以前的文件名上限小的时候比如14,此时文件名过长会截断, 也可能会返回错误值, 这样就出现了很大的风险, 系统也不知道到底截断过还是返回错误值, 现在不用考虑这个问题了, 现在的文件名上限大多255

3.4 函数create

1
2
3
4
5
6
#include <fcntl.h>
int create(const char* path, mode_t mode);
//成功返回只写打开的文件描述符, 失败返回-1
//此函数等效于
open(path, O_WRONLY | O_CREATE | O_TRUNC, mode);
//create出现的原因在于早期,open不支持O_CREATE

另一个不足之处, 在于create只能创建只写打开的文件

3.5 函数close

1
2
3
#include <unistd.h>
int close(int fd);
//成功返回0, 失败返回-1

关闭一个文件时还会释放该进程加在该文件上的所有记录锁,当一个进程终止时, 内核自动关闭所有它打开的文件

3.6 函数lseek

1
2
3
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
//成功返回文件偏移量, 失败返回-1

对参数offset的解释与参数whence的值有关:

  1. 若whence是SEEK_SET, 将文件的偏移量设置为距文件开始offset个字节
  2. 若whence是seek_cur, 将文件的偏移量设置为当前值加offset, offset可正可负
  3. 若whence是SEEK_END, 将文件的偏移量设置为文件长度加offset, 可正可负
1
2
3
4
//获取当前偏移量
off_t currpos;
currpos = lseek(fd, 0, SEEK_SET);
//这种方法也可以用来确定涉及文件是否可以设置偏移量, 如果文件描述符是管道, fifo 或套接字, lseek返回-1, 并将errno设置为ESPITE

文件偏移量可以大于文件的当前长度, 这种情况下会形成空洞文件,但位于文件中没有写过的字节都被读成0

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 <fcntl.h>

char buf1[] = "abcdefghij";
char buf2[] = "ABCDEFGHIJ";

int main() {
int fd;

if ((fd = create("file.hole", FILE_MODE)) < 0)
err_sys("create error");

if (write(fd, buf1, 10) != 10)
err_sys("write error");
/* offset now = 10*/

if (lseek(fd, 16384, SEEK_SET) == -1)
err_sys("lseek error");
/* offset noew = 16384 */

if (write(fd, buf2, 10) != 10)
err_sys("buf2 write error");
/* offset noew = 16394 */

exit(0);
}

3.7 函数read

1
2
3
#include <unistd.h>
ssize_t read(int fd, void* buf, size_t nbytes);
//读到的字节数,若已到文件尾,返回0, 失败返回-1

有多种情况可使实际读到的字节数少于要求读的字节数:

  1. 读普通文件时, 在读到要求的字数之前已到达文件尾端, 例如文件30字节, 而要求读100字节, 则read返回30, 下一次调用返回0
  2. 从终端设备读时,通常一次最多读一行
  3. 从网络读时, 取决于网络中的缓冲机制
  4. 从管道和FIFO读时, 返回实际读到的可用字节数
  5. 从某些面向记录的设备时(如磁带), 一次最多返回一个记录
  6. 当一信号造成中断时, 而已读了部分数据

3.8 函数write

1
2
3
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t nbytes);
//成功返回已写字节数, 失败返回-1

对于普通文件, 写操作从文件的当前偏移量处开始,如果在打开文件时,指定了O_APPEND选项,每次写操作之前,将文件的偏移量设置在文件的当前结尾处,在一次成功写操作后,文件偏移量增加实际写的字节数

3.9 I/O的效率

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <unistd.h>
#define BUFFSIZE 4096

int main() {
int n;
char buf[BUFFSIZE];

while ((n == read(STDIN_FILENO, buf, BUFFSIZE)) > 0)
if (write(STDOUT_FILENO, buf, n) != n)
err_sys("write error");

if (n < 0)
err_sys("read error");

exit(0);
}

对于不同大小的BUFFSIZE

image-20221018231151437

大多数文件系统为改善性能采用某种预读技术

3.10 文件共享

本小节分两部分,第一部分描述了内核中文件是以何等方式打开文件

  1. 每个进程在进程表中都有一个记录项,记录项包含一张打开的文件描述符表,每个描述符占用一项,与每个文件描述符关联的是

    ​ a.文件描述符标志(close_on_exec)

    ​ b.指向一个文件表项的指针

  2. 内核为所有打开文件维持一张表,每个表项包含:

    ​ a.文件状态标志(读,写,添加,同步和非阻塞等)

    ​ b.当前文件偏移量

    ​ c.指向该文件v节点表项的指针

  3. 每个打开文件都有一个v节点结构,v节点包含了文件类型和对此文件进行各种操作函数的指针,对于大多数文件还包含i节点,索引节点

image-20221018235724033

另一部分说明了,如果两个独立进程各自打开了同一文件:

image-20221018235917212

我们假定第一个进程在3上打开了文件,而另一个在4上打开了文件,每个进程都会获得一份自己的文件表项,但一个文件只有一个v节点表项

fork后的父子进程,各自的每一个打开文件描述符共享同一个文件表项

3.11 原子操作

简单来说,在多进程的背景下,两次函数调用之间,可能会出现,从当前进程切到另一进程,执行另一进程的事情后,再返回当前进程继续往下执行,这期间可能会导致问题

1
2
3
4
5
6
//一个追加文件的例子
if (lseek(fd, OL, 2) < 0) //假设设置偏移量到文件尾
err_sys("lseek error");
if (write(fd, buf, 100) != 100)
err_sys("write error");

此时就可能会出现这么一种情况,当前进程将偏移量设置到文件尾后,假设为1500,此时由于调度切换到另一进程,另一进程对同一文件进行了修改,使得文件变成了1600大小,此时再切会当前进程,就出现了和我没所期望的不一样的结果,而将lseek和write的作用合成一步,这就是原子操作,如UNIX系统为我们提供了一种方法,O_APPEND

1
2
3
4
5
6
7
//XSI扩展的原子性地定位并执行I/O
#include <unistd.h>
ssize_t pread(int fd, void* buf, size_t nbytes, off_t offset);
//
ssize_t pwrite(int fd, void*buf, size_t nbytes, off_t offset);

//相当于先lseek再调用read/write

3.12 函数dup和dup2

1
2
3
4
5
//下列两个函数可用来复制一个现有的文件描述符
#include <unistd.h>
int dup(int fd);
int dup2(int fd, int fd2);
//两函数的返回值,若成功返回新的文件描述符,若出错返回-1

dup返回的新文件描述符一定是当前可用文件描述符中的最小值,对于dup2,可用用fd2指定新文件描述符的值,如果fd2已经打开,先将其关闭。如果fd等于fd2,则dup2返回fd2,而不关闭它。否则,fd2的FD_CLOEXEC文件描述符标志就被清除(?不理解什么意思)

image-20221019002908225

1
2
3
4
//对于dup 和 dup2 同样也有等效函数
dup(fd); -----> fcntl(fd, F_DUPFD, 0);
dup2(fd, fd2);-----> close(fd2); fcntl(fd, F_DUPFD, fd2);
//区别在于: 1.dup2是原子操作 2.errno不同

3.13 函数sync,fsync 和 fdatasync

传统的UNIX系统实现在内核中没有缓冲区高速缓存或页高速缓存。写入数据时,先将数据复制到缓冲区,然后排入队列,再写入磁盘。该方式被称为延迟写。sync, fsync和fdatasync三个函数用来保证磁盘实际文件系统和缓冲区内容的一致性

1
2
3
4
5
#include <unistd.h>
int fsync(int fd);
int fdtasync(int fd);
//成功返回0, 失败返回-1
void sync(void);

sync只是将修改过的块缓冲区排入写队列,然后返回,不等待实际写磁盘操作结束,被称为update的守护进程,会周期性调用该函数

fsync只对fd指定的文件起作用,并且等待实际写硬盘操作结束

fdtasync与fsync类似,但它只影响文件的数据部分,而除数据外,fsync还会同步更新文件属性

3.14 函数fcntl

1
2
3
//fcntl函数可以改变已经打开的文件属性
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /*int arg*/);

fcntl函数有以下5个功能:

  1. 复制一个已有的描述符(cmd = F_DUPFD 或 F_DUPFD_CLOEXEC)
  2. 获取/设置文件描述符标志(cmd = F_GETFD 或 F_SETFD)
  3. 获取/设置文件状态标志(cmd = F_GETFL 或 F_SETFL)
  4. 获取/设置异步I/O所有权(cmd = F_GETOWN 或 F_SETOWN)
  5. 获取/设置记录锁 (参考上面视频笔记部分的文件锁,很详细,后续也会继续补全)

该小节只说明前四种,记录锁在后面章节补充:

**F_DUPFD:**复制文件描述符fd,新文件描述符作文函数值返回,它是尚未打开的各描述符中大于或等于第三个参数值中各值的最小值(就是从传入的fd2起,没有被使用的最小描述符),他与fd共享同一文件表项,但它有自己的一套文件描述符标志,其FD_CLOEXEC设置为被清除(?不清楚),第8章会讨论这一点

**F_DUPFD_CLOEXEC:**复制文件描述符fd,设置与新描述符关联的FD_CLOEXEC文件描述符标志的值,返回新文件描述符

**F_GETFD:**对应于fd的文件描述符标志作为函数值返回,当前只定义了一个描述符标志FD_CLOEXEC(主要还不知的文件描述符标志用来干啥,后续应该会补充)

**F_SETFD:**对于fd设置的文件描述符标志,新标志值按第三个参数设置

​ ”现在很多程序不使用常量FD_CLOEXEC,而是设置为0, (系统默认,在exec时不关闭), 设置为1(在exec时关闭)“

**F_GETFL:**对应于fd的文件状态标志作为函数值返回,open时已经描述了文件状态

image-20221019005856046

对于5个访问标志,前5个,并不各占一位,由于历史原因,前3个的值分别是0,1,2,因此首先必须使用屏蔽字O_ACCMODE取得访问方式位

**F_SETFL:**将文件状态的设置为第3个参数的值,可以更改的几个标志为:O_APPEND, O_NONBLOCK, O_SYNC, O_DSYNC, O_RSYNC, O_FSYNC, O_ASYNC

**F_GETOWN:**获取当前接受SIGIO和SIGURG信号的进程ID或进程组ID,14章会作讨论

**F_SETOWN:**设置接收SIGIO和SIGURG信号的进程ID或进程组ID,正的arg指定一个进程ID,负的arg指定进程组ID

fcntl的返回值与命令有关,出错所有都返回-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
//示例
#include <fcntl.h>

int main(int argc, char* argv[]) {
int val;

if (argc < 2)
err_quit("usage: a.out <descriptor#>");

if ((val = fcntl(atoi(argv[1]), F_GETFL, 0)) < 0)
err_sys("fcntl error");

switch (val & O_ACCMODE) {
case O_RDONLY:
printf("read only");
break;
case O_WRONLY:
prinf("write only");
break;
case O_RDWR:
printf("read write");
break;
default:
err_dump("unknown access mode");
}

if (val & O_APPEND)
printf(", append");
if (val & O_NONBLOCK)
printf(", nonblocking");
if (val & O_SYNC)
printf(", synchronous writes");

putchar('\n');
exit(0);
}

//结果
$./a.out 0 < /dev/tty
read only
$./a.out 1 > temp.foo
$cat temp.foo
write only
$./a.out 2 2>>temp.foo
write only, append
$./a.out 5 5<>temp.foo
read write

修改时应该采用先获取现在的标志值,然后修改它,最后设置新的标志值,而不是之间F_SETFD或F_SETFL这样会关闭以前设置的标志值

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

//flags are file status flags to turn on
void set_fl(int fd, int flags) {
int val;
if ((val = fcntl(fd, F_GETFL, 0)) < 0)
err_sys("fcntl get error");
val |= flags;
if (fcntl(fd, F_SETFL, val) < 0)
err_sys("fcntl set error");
}
//如果这样就是关闭 turn off
vla &= ~flags;

3.15 函数ioctl

ioctl函数一直是I/O操作的杂物箱,不能用本章其他函数表示的I/O操作通常都能用ioctl表示,终端I/O是使用ioctl最多的地方(what? 没用过难以理解是干啥的)

1
2
3
4
#include <unistd.h>
#include <sys/ioctl.h>

int ioctl(int fd, int request, ...);

后续描述看起来ioctl是一个能自定义一些I/O操作的函数,如磁带操作使我们可以在磁带上写一个文件结束标志,倒带,越过指定个数的文件或记录等。本章其他函数操作都难以表示这些操作,所以最容易的方法就是使用ioctl

3.16 /dev/fd

了解了解就行,举个例子,fd = open(“dev/fd/0”, mode) 相当于fd = dup(0);

小结

本章主要说明了UNXI系统提供的基本I/O函数,原子操作,内核共享打开文件信息的数据结构,多种将数据冲洗到磁盘上的方式(sync?),不同I/O长度对读文件所需时间的影响,已经多种功能的fcntl函数,14章还将介绍fcntl的第5中功能,18,19章介绍ioctl函数

第4章 文件和目录

4.1 引言

本章进一步阐述文件系统的一些相关内容

4.2 函数stat、fstat、fstatat和lstat

1
2
3
4
5
6
#include <sys/stat.h>
int stat(const char *restrict pathname, struct stat *restrict buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *restrict pathname, struct stat *restrict buf);
int fstatat(int fd, const char *restrict pathname, struct stat *restrict buf, int flag);
//所有4个函数,成功返回0,失败返回-1

四个函数功能基本一样,stat和fstat的区别在于使用文件名还是文件描述符,lstat的区别在于是否穿透,即是返回符号链接的信息还是符号链接所指文件的信息(lstat返回符号链接本身的信息,stat返回所指文件的信息),fstatat更像是一个集合体,fd为打开目录,pathname为文件名,当pathname为绝对路径时,fd被忽略,flag的取值绝对是否追踪符号链接,默认追踪,当被设置为AT_SYMLINK_NOFOLLOW时不追涨。 buf为一个传出参数,实际结构是一个包含了文件个信息的结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//struct stat 的结构各实现可能不同,但大体内容基本如下
struct stat {
mode_t st_mode; /*file type & mode (permissions) */
ino_t st_ino; /*i_node number (serial number) */
dev_t st_dev; /*device number (file system) */
dev_t st_rdev; /*device number for special files */
nlink_t st_nlink; /*number of links */
uid_t st_uid; /*user ID of owner */
gid_t st_gid; /*group ID of owner */
off_t st_size; /*size in bytes, for regular files */
struct timespec st_atime; /*time of last access */
struct timespec st_mtime; /*time of last modification */
struct timespec st_ctime; /*time of last file status change */
blksize_t st_blksize; /*best I/O block size */
blkcnt_t st_blocks; /*number of disk blocks allocated */
}

struct timespec {
time_t tv_sec; //秒
long tv_nsec; //纳秒
}

4.3 文件类型

UNXI系统中文件的七种类型

**普通文件:**最常用的文件,包含某种形式的数据,至于是文本还是二进制,对于UNXI内核并无区别

目录文件:只有内核可以之间写目录,进程必须使用当前章节的介绍的函数才可以更改目录

**块特殊文件:**这种类型的文件提供对设备带缓冲的访问,每次访问以固定长度为单位进行

**字符特殊文件:**这种类型的年纪提供对设备不带缓冲的访问,每次访问长度可变,系统中的所有设备要么是块特殊文件,要么是字符特殊文件

**FIFO:**命名管道,用于进程间通信,上面视频笔记小实验聊天室用到了

**套接字:**用于进程间的网络通信,也可以用于在一台宿主机上的非网络通信,16章

**符号链接:**这种类型文件指向另一个文件,类似快捷方式

1
2
3
4
5
6
7
8
9
//文件类型信息包含在stat结构体的st_mode成员中,可以用宏来确定
//宏 文件类型
S_ISREG() 普通文件
S_ISDIR() 目录文件
S_ISCHR() 字符特殊文件
S_ISBLK() 块特殊文件
S_ISFIFO() 管道或FIFO
S_ISLNK() 符号链接
S_ISSOCK() 套接字
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
//示例,命令行参数打印其文件类型
#include <unistd.h>

int main(int argc, char *argv[]) {
int i;
struct stat buf;
char *ptr;

for(i = 1; i < argc; i++) {
printf("%s: ", argv[i]);
if (lstat(argv[i], &buf) < 0) {
err_ret("lstat error");
continue;
}
if (S_ISREG(buf.st_mode))
ptr = "regular";
else if (S_ISDIR(buf.st_mode))
ptr = "directory";
.
.
.
else if (S_ISSOCK(buf.st_mode))
ptr = "socket";
else
ptr = "** unknown mode **";
printf("%s\n", ptr);
}
exit(0);
}

4.4 设置用户ID和设置组ID

与一个进程相关联的ID有6各或更多

1
2
3
4
5
6
7
8
9
实际用户ID	              //我们实际上是谁
实际组ID

有效用户ID
有效组ID //用于文件访问权限检查
附属组ID

保存的设置用户ID //由exec函数保存
保存的设置组ID

通常,有效用户ID等于实际用户ID,有效组ID等于实际组ID。每个文件有一个所有者和所有组,所有者由stat结构中的st_uid指定,组所有者则由st_gid指定

当执行一个程序文件时,进程的有效ID通常就是实际用户的ID,有效组ID通常是实际用户的ID,但可以在文件模式字(st_mode)中设置一个特殊标志,含义是“当执行此文件时,进程的有效用户ID设置为文件所有者的用户ID”,同理还有另一位设置组ID是否是文件所有组,这两位被称为 设置用户ID位和设置组ID位

4.5 文件访问权限

每个文件有9个访问位权限:

1
2
3
4
5
6
//用户
S_IRUSR 用户读 S_IWUSR 用户写 S_IXUSR 用户执行
//组
S_IRGRP 组读 S_IWGRP 组写 S_IXGRP 组执行
//其他人
S_IROTH 其他读 S_IWOTH 其他写 S_IXOTH 其他执行

image-20221019140753392

由ls-l 得到的某文件的权限设置 prw-rw-r-x 第一个符号为文件类型,后面三个一组分别为用户,组,其他人,rwx对应读写执行权限,-说明没有权限

4.6 新文件和目录的所有权

新文件:

  1. 新文件的用户ID设置为进程的有效ID
  2. 新文件的组id

​ a.新文件的组ID可以是进程的有效组ID

​ b.新文件的组ID可以是它所在目录的组ID

4.7 函数access和faccessat

按实际对应的用户ID和实际组ID进行访问权限测试

1
2
3
4
5
#include <unistd.h>
int access(const char *pathname, int mode);
int faccessat(int fd, const char *pathname, int mode, int flag);
//两个函数的返回值,成功返回0,失败返回-1
mode: R_OK(读) W_OK(写) X_OK(执行)

access和faccessat在以下两种情况是相同的:

  1. pathname取绝对路径
  2. fd参数取值AT_FDCWD,pathname取相对路径(大部分类似参数的两种函数都类似)

flag参数用于改变faccessat的行为,设置为AT_EACCESS访问调用进程的有效ID和有效组ID而不是实际的,例如设置当前进程用户超级用户权限

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

int main(int argc, char *argv[]) {
if (argc != 2)
err_quit("usage: a.out<pathname>");
if (access(argv[1], R_OK) < 0)
err_ret("access error for %s", argv[1]);
else
printf("read access OK\n");
if (open(argv[1], O_RDONLY) < 0)
err_ret("open error");
else
printf("open for reading OK\n");
}

4.8 函数umask

1
2
3
#include <sys/stat.h>
mode_t umask(mode_t cmask);
//返回之前文件模式创建的屏蔽字

简单来说,你创建的文件实际权限会受到umask掩码的影响,算是一种保护文件权限的预防措施,举个例子

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
#include <fcntl.h>
#define RWRWRW (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH)

int main() {
umask(0);
if (create("foo", RWRWRW) < 0)
err_sys();
umask(S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
if (create("bar"m RWRWRW) < 0)
err_sys();
exit(0);
}

//运行结果
$ umask 打印之前的屏蔽字
002
$ ./a.out
$ ls-l foo bar
-rw------- 1 sar 0 Dec 7 21:20 bar
-rw-rw-rw- 1 sar 0 Dec 7 21:20 foo

//也可以不用宏,直接用八进制格式来指定,但注意rwx分别对应的值是4,2,1
$ umask
002
$ umask -s //打印符号格式
u = rwx, g = rwx, o = rx
$ umask 027
$ umask -s
u = rwx, g = rx, o =

4.9 函数chmod、fchmod和fchmodat

1
2
3
4
5
#include <sys/stat.h>
int chmod(const char *pathname, mode_t mode);
int fchmod(int fd, mode_t mode);
int fchmodat(int fd, const char *pahtname, mode_t mode, int flag);
//成功返回0,返回-1

fchmodat,对于flag参数,设置为AT_SYMLINK_NOFOLLOW标志时,fchmodat不会跟随符号链接

image-20221019180705396

对于S_ISUID、S_ISGID、这两个是强制为权限,以s表示,如果在user权限组中设置了s位,则当文件被执行时,该文件是以文件所有者UID而不是用户UID执行程序;如果在group权限组中设置了s位,当文件被执行时,该文件是以文件所有者GID而不是用户GID执行程序。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
#include <sys/types.h>
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>

int main() {
struct stat statbuf;

stat("foo", &statbuf);
if (chmod("foo",(statbuf.st_mode & ~S_IXGRP) | S_ISGID) <0 ) {
perror("chmod 1 error");
return 0;
}

chmod("bar", S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
return 0;
}

//运行前
aurora@LAPTOP-1GSU2G27:~/learning/Day1019$ ls -l bar foo
-rw------- 1 aurora aurora 0 Oct 19 17:44 bar
-rw-rw-rw- 1 aurora aurora 0 Oct 19 17:44 foo
//运行后
aurora@LAPTOP-1GSU2G27:~/learning/Day1019$ ls -l bar foo
-rw-r--r-- 1 aurora aurora 0 Oct 19 17:44 bar
-rw-rwSrw- 1 aurora aurora 0 Oct 19 17:44 foo

chmod函数还会自动清楚两个权限位(p86)

4.10 粘着位

早期的一种技术,用于将文件存放在交换区,这样下次执行的时候就能更快的装载如内存,现在不太需要这种技术了

现今系统扩展了黏着位的适用范围,Single UNIX Specification允许针对目录设置黏着位。如果对一个目录设置了黏着位,只有对该目录具有写权限,并且满足下列条件之一,才能删除或重命名该目录下的文件

  1. 拥有此文件
  2. 拥有此目录
  3. 是超级用户

例如目录/tmp 和 目录/var/tmp

4.11 函数chown、fchown、fchownat和lchown

下面几个chown函数可用于更改文件的用户ID和组ID,如果两个参数任意一个是-1,则对应的ID不变

1
2
3
4
5
6
#include <unistd.h>
int chown(const char *pathname, uid_t owner, gid_t group);
int fchown(int fd, uid_t owner, gid_t group);
int fchownat(int fd, const char *pathname, uid_t owner, gid_t group, int flag);
int lchown(const char *pathname, uid_t owner, gid_t group);
//4个函数的返回值 成功返回0,失败返回-1

4个函数基本一样,主要区别也就是fd和pathname的取舍,是否穿透符号链接,和前面类似形参的不同函数区别基本一样p88

4.12 文件长度

对于普通文件,长度可以是0,对于目录文件,文件长度通常是一个数(如16或512)的整数倍,对于符号链接,文件长度是文件名的字节数

文件中的空洞:

形成的原因,偏移量超过实际长度,并且继续写入了部分数据

1
2
3
4
5
//考虑以下情况
$ ls -l core
-rw-r--r-- 1 sar 8483248 Nov 18 12:18 core
$ du -s core
272 core

文件的长度超过8mb,但使用的磁盘空间总量272个512字节块(139264字节),很明显该文件有很多空洞

4.13 文件截断

有时我们需要在文件尾端截去一些数据以缩短文件,将一个文件截断为0,可以在打开时通过O_TRUNC标志做到。为了截断文件可以调用truncate和ftruncate

1
2
3
4
#include <unistd.h>
int truncate(const char *pathname, off_t length);
int ftruncate(int fd, off_t length);
//两个函数的返回值,成功返回0,失败返回-1

如果长度大于length,截断,如果小于length,文件长度增加,相当于增加的部分创建了空洞文件

4.14 文件系统

本小节简单讲了下,UNIX系统的经典文件系统格式,重点在于i节点和数据块,数据块由目录块和数据块组成,由此可以看出文件真正位置与目录无关,目录的每个目录项储存的是文件的i节点编号和文件名,通过i节点编号来找到实际的数据块,目录块会根据类型字段判断目录项里是否还是一个目录,对于一个没有包含其他目录的叶目录的链接计数总是2(理解:每个目录中都存在一个. 目录,指向当前目录,然后又有创建这个也目录的目录也能指向当前目录,所以链接计数为2),这里的一点疑惑,这个叶目录,在创建它目录的目录项中的i 节点编号(貌似是指向它自己的实际目录块,所以因为没创建文件所以只有. 和 ..目录项)

image-20221020135343695

image-20221020135409469

image-20221020135422426

4.15 函数link、linkat、unlink、unlinkat和remove

创建一个指向现有文件的链接

1
2
3
4
#include <unistd.h>
int link(const char *existingpath, const char *newpath);
int linkat(int efd, const char *existingpath, int nfd, const char *newpath, int flag);
//成功返回0,失败返回-1

两函数差不多,差别仍然是pathname 和 fd + pathname 的区别, flag用来区别是否追踪符号链接,如果设置AT_SYMLINK_FOLLOW创建指向符号链接目录的链接,如果忽略,创建一个指向符号链接本身的链接,创建目录项和增加链接计数为原子操作

1
2
3
4
#include <unistd.h>
int unlink(const char *pathname);
int unlinkat(int fd, const char *pathname, int flag);
//成功返回0,失败返回-1

这两个函数删除目录项,并将链接计数-1,类似share_ptr 只有计数为1的时候,文件内容才被删除,不为0时仍可通过其他链接访问,如果目录被设置为黏着位了,必须满足黏着位的要求才能操作(参考前面),flag参数给出一种方法,可以改变unlinkat函数的默认行为,当AT_REMOVEDIR标志被设置时,unlinkat可以类似于rmdir一样删除目录,如果标志被清除,unlinkat和unlink执行同样的操作

unlink的一种特性,在程序运行中,对打开的unlink文件不会被立马清除,只有在进程结束的时候才会将其删除,利用这种特性,对打开的临时文件unlink可以防止程序崩溃时,遗留临时文件

1
2
3
#include <unistd.h>
int remove(const char *pathname);
//对于文件,remove的功能与unlink相同, 对于目录,与rmdir相同

4.16 函数rename和renameat

文件或目录可以用rename和renameat重命名

1
2
3
4
#include <stdio.h>
int rename(const char *oldname, const char *newname);
int renameat(int oldfd, const char *oldname, int newfd, const char *newname);
//成功返回0,失败返回-1

一些注意点见p96

4.17 符号链接

引入符号是对一个文件的间接指针,引入符号链接的原因,为了避免硬链接的限制

  1. 硬链接通常要求链接和文件位于同一文件系统中
  2. 只有超级用户才能创建指向目录的硬链接(在底层文件系统支持的情况下)

不过要注意函数是否能处理符号链接,就像之前讲过的通过flag的参数来设置是否追踪

image-20221020144528852

上图的一个例外是,open函数同时调用O_CREATE和O_EXCL时,如果引用符号链接,open将出错返回,errno设置为EEXIST,目的是为了堵塞一个安全漏洞,防止特权进程被诱骗写错误文件

后续讲了一个软连接导致文件系统中引入循环的例子,就目录里创建一个软连接,然后这个软链接又指向目录,这样的循环可以用unlink消除,但硬链接很难消除,所以不允许普通用户构造指向目录的硬链接

4.18 创建和读取符号链接

可以用symlink或symlinkat创建符号链接

1
2
3
4
#include <unistd.h>
int symlink(const char *actualpath, const char *sympath);
int symlinkat(const char *actualpath, int fd, const char *sympath);
//成功返回0,失败返回-1

因为open函数跟随符号链接,所以须有一种方法打开符号链接本身,读取该链接的名字

1
2
3
4
#include <unitstd.h>
ssize_t readlink(const char *restrict pathname, char *restrict buf, size_t bufsize);
ssize_t readlinkat(int fd, const char *restrict pathname, char *restrict buf, size_t bufsize);
//成功返回读取的字节数,失败返回-1

这两个函数组合了open、read和close的所有功能,一气呵成嗷

4.19 文件的时间

文件的时间类型有三种

  1. st_atime 文件数据的最后访问时间 例如: read
  2. st_mtime 文件数据的最后修改时间 例如:write
  3. st_ctime i节点状态的最后更改时间 例如:chmod chown

image-20221020151050415

4.20 函数futimens、utimensat和utimes

一个文件的访问和修改时间可以用以下几个函数更改

1
2
3
4
#include <sys/stat.h>
int futimens(int fd, const struct timespec times[2]);
int utimensat(int fd, const char *path, const struct timespec times[2], int flag);
//成功返回0,失败返回-1

times数组的第一个元素包含访问时间,第二个元素包含修改时间

时间戳按以下方式指定:

  1. 如果times参数是空指针,访问和修改时间设置为当前时间
  2. 如果非空,任一数组元素的tv_nsec字段的值为UTIME_NOW,相应的时间戳设置为当前时间,忽略tv_sec
  3. 如果非空,任意数组元素的值为UTIME_OMIT,相应时间戳保持不变,忽略tv_sec
  4. 如果非空,且tv_nsec字段既不是UTIME_NOW,也不是UTIME_OMIT,按相应的设置

执行这些函数所要求的优先权取决于times参数的值

  1. 如果是空指针,或者任一tv_nsec字段设为UTIME_NOW,则进程的有效用户ID必须等于该文件的所有者ID;进程对该文件必须具有写权限,或者超级用户进程
  2. 如果非空,且既不是UTIME_NOW也不是UTIME_OMIT,进程的有效用户ID必须等于文件所有者ID,或者是超级用户进程,只有写权限是不够的
  3. 如果非空,两个字段的值都为UTIME_OMIT,不执行任何权限检查

flag同样用来对于符号链接进行区分,设置AT_SYMLINK_NOFOLLOW,只修改符号链接本身,默认行为是跟随符号链接,并把文件的的时间改成符号链接的时间

1
2
3
4
5
6
7
#include <sys/time.h>
int utime(const char *pathname, const struct timeval times[2]);

struct timeval {
time_t tv_sec; //秒
long tv_usec; //微秒 timespec是纳秒
}

示例:p102

4.21 函数mkdir、mkdirat和rmdir

用mkdir和mkdirat来创建目录

1
2
3
4
#include <sys/stat.h>
int mkdir(const char *pathname, mode_t mode);
itn mkdirat(int fd, const char *pathname, mode_t mode);
//成功返回0,失败返回-1

注意目录mode至少需要拥有执行权限

1
2
3
#include <unitstd.h>
int rmdir(const char *pathname);
//成功返回0,失败返回-1

如果有进程打开目录,不会立马删除,但也不允许创建新文件,

4.22 读目录

目录的实际格式依赖于UNIX系统实现和文件系统的设计,早期较简单的结构,每个目录项16个字节,14字节是文件名,2字节是i节点编号,就如同前面文件系统中描述的那种

任何具有访问权限的用户都可以读目录,但只有内核才对目录具有写权限,目录的写和执行权限决定的是能否在目录中创建和删除文件,不带表写目录本身,此外,很多实现阻止应用程序使用read函数读取目录的内容,由此进一步讲应用程序与目录格式中与实现相关的细节隔离

1
2
3
4
5
6
7
8
9
10
11
12
#include <dirent.h>
DIR* opendir(const char *pathname);
DIR* fdopendir(int fd);
//两函数成功返回指针,失败返回NULL
struct dirent* readdir(DIR *dp);
//成功返回指针,若在目录尾或失败返回NULL
void rewinddir(DIR* dp);
int closedir(DIR* dp);
//成功返回0,失败返回-1
long telldir(DIR* dp);
//返回与dp关联的目录中的当前位置
void seekdir(DIR* dp, long loc);

image-20221020154756395

1
//示例 p105 有时间补充

4.23 函数chdir、fchdir和getcwd

进程调用chdir,fchdir可以修改当前工作目录

1
2
3
4
#include <unistd.h>
int chdir(const char *pathname);
int fchdir(int fd);
//成功返回0,失败返回-1

从当前目录,用..找到上一级目录,,逐层上移,得到当前工作目录的完整绝对路径

1
2
3
#include <unitstd.h>
char* getcwd(char *buf, size_t size);
//成功返回buf,失败返回NULL

4.24 设备特殊文件

st_dev和st_rdev ,在18.9节,编写ttyname函数时,需要使用这两个字段,相关规则:

  1. 每个文件系统所在的存储设备都由其主、次设备号表示。设备号所用的数据类型是基本系统数据类型dev_t。主设备号标识设备驱动程序,有时编码为与其通信的外设板;次设备号标识特定的子设备。如4-13图,一个磁盘的驱动器经常包含若干个文件系统。在同一磁盘驱动器上的各文件通常具有相同的主设备号,但是次设备号却不同
  2. 我们通常可以使用两个宏:major和minor来访问主、次设备号,大多数实现都定义这两个宏。这就意味着我们无需关心这两个数是如何存放在dev_t对象中的
  3. 系统中与每个文件名关联的st_dev值是文件系统的设备号,该文件系统包含了这一文件名以及与其对应的i节点
  4. 只有字符特殊文件和块特殊文件才有st_rdev值,此值包含实际设备的设备号

p112有个示例,但还不明白设备特殊文件是啥,先跳过

4.25 文件访问权限小结

所有的文件访问权限位总结

1
2
3
4
S_IRWXU = S_IRUSR | S_IWUSR | S_IXUSR
S_IRWXG = S_IRGRP | S_IWGRP | S_IXGRP
S_IRWXO = S_IROTH | S_IWOTH | S_IXOTH
//剩下的太多直接放图

image-20221020160917814

4.26 小结

本章围绕stat函数,介绍了stat中的每一个成员,以及各种对文件目录操作的函数,和文件系统的基本结构实现

第5章 标准I/O库

5.1 引言

标准I/O库处理很多细节,如缓冲区分配,以优化的块长度执行I/O等。这些处理使用户不用担心如何选择使用正确的块长度(如3.9节所)。这使得它便于用户使用,但是如果不深入地了解I/O库函数的操作,也会带来问题

5.2 流和FILE对象

在第三章时的I/O函数围绕文件描述符来进行。而对于标准I/O库,它们的操作是围绕流进行的(勿讲标准I/O库的术语流与System V的STREAMS I/O 系统混淆)。当我们使用标准I/O库打开或创建一个文件的时候,我们已使一个流与一个文件相关联。

对于不同字符,可能用不同字节来表示,流的定向决定了所读、写的字符是单字节还是多字节。当一个流被创建的时候,它并没有定向,根据使用多字节I/O函数还是单字节I/O函数来设定定向。只有两个函数可以改变流的定向,freopen函数清除一个流的定向;fwide函数设置流的定向

1
2
3
4
#include <stdio.h>
#include <wchar.h>
int fwide(FILE *fp, int mode);
//若流是宽定向,返回正值;字节定向,返回负值;未定向返回0

根据mode参数的不同值,执行不同工作:

  1. 若mode参数为负值,fwide试图指定流是字节定向
  2. 若为正,试图指定宽定向
  3. 若为0,不试图设置,但返回标识该流定向的值

5.3 标准输入、标准输出和标准错误

每一个进程预定义了3个流,并且自动地被使用,引用的文件就是之前提到过的STDIN_FILENO、STDOUT_FILENO、STDERR_FILLNO。这3个标准I/O流通过预定义文件指针来引用

5.4 缓冲

目的和之前3.9节差不多,尽可能减少read和write的调用次数。标准I/O库的缓冲是自动地进行缓冲管理,避免用户需要考虑怎么设置

标准I/O提供了以下3种类型的缓冲:

  1. 全缓冲。在这种情况下,填满标准I/O缓冲区后才进行实际I/O操作,通常在一个流执行第一次I/O操作时,相关函数调用malloc获取所需的缓冲区

    术语冲洗(flush)说明标准I/O缓冲区的写操作,注意在UNIX环境中,flush可能有两种意思,标准I/O库方面意味着将缓冲区内容写入磁盘;在终端驱动程序表示丢弃已存储在缓冲区中的数据

  2. 行缓冲。在输入和输出中遇到换行符时,标准I/O库执行I/O操作,当流涉及一个终端(标准输入和标准输出)时,通常使用行缓冲

    行缓冲的两个限制:a.行缓冲区的长度是固定的,只要填满了,不管有米有换行符,也进行I/O操作 b.标准I/O库要求从一个不带缓冲的流或一个行缓冲的流中获取数据时,也会立刻flush

  3. 不带缓冲。标准I/O库不对字符进行缓冲储存,例如,若用标准I/O函数fputs写15个字符到不带缓冲的流,我们期望这15个字符能立刻输出,很可能使用3.8节的write函数将其写到相关文件

通常标准错误流不带缓冲,使得报错信息立刻输出。ISO C要求下列缓冲特征:

  1. 当且仅当标准输入和标准输出并不指向交互式设备时,它们才是全缓冲的
  2. 标准错误绝对不会是全缓冲

很多系统默认下列类型缓冲:

  1. 标准错误是不带缓冲的
  2. 若是指向终端设备的流,则是行缓冲,否则是全缓冲
1
2
3
4
5
//下列函数用来更改系统默认缓冲类型
#include <stdio.h>
void setbuf(FILE *restrict fp, char *restrict buf);
int setvbuf(FILE *restrict fp, char *restrict buf, int mode, size_t size);
//成功返回0,失败返回-1

setbuf用来简单设定,全缓冲还是不带缓冲,buf的大小由常量BUFSIZ决定,关闭传NULL,如果与终端设备相关,则可能将其设置为行缓冲。

setvbuf用来精确设定,借用mode参数实现: 1._IOFBF(全缓冲) 2. _IOLBF(行缓冲) 3. _IONBF(不带缓冲)

image-20221020201531890

1
2
3
#include <stdio.h>
int fflush(FILE *fp);
//强制冲洗一个流,若fp == NULL, 冲刷所有输出流

5.5 打开流

下列三个函数用来打开一个标准I/O流

1
2
3
4
5
#include <stdio.h>
FILE* fopen(const char *restrict pathname, const char *restrict type);
FILE* freopen(const char * restrict pathname, const char *restrict type, FILE *restrict fp);
FILE* fdopen(int fd, const char *type);
//成功返回文件指针,失败返回NULL

三函数的区别:

  1. fopen打开pathname的一个指定文件,type为打开方式
  2. freopen在一个指定的流上打开一个指定的文件,若流已打开,则先关闭流。若该流已定向,清除其定向,此函数一般用于将一个指定文件打开为预定义的流:标准输入,标准输出或标准错误
  3. fdopen根据文件描述符打开一个流,通常用于创建管道和网络通信通道函数返回的描述符,因为这种特殊类型不能用标准I/O函数fopen打开

image-20221020204317293

字符b在UNIX环境下无意义,因为UNIX内核不区分文本文件和二进制文件。对于fdopen,type的参数稍有区别,因为该描述符已经被打开了,区别主要在于截断和创建都不会起作用,因为已经打开,文件已经存在了

当以读和写类型打开一个文件时,具有以下限制: (不太理解什么意思?)

  1. 如果中间没有fflush, fseek, fsetpos或rewind,则在输入之后不能直接根输入
  2. 如果中间没有fseek, fsetpos或rewind,或者输入操作没有到达文件尾端,输入操作后不能跟输出

image-20221020204820635

1
2
3
4
//fclose关闭一个流
#include <stdio.h>
int fclose(FILE *fp);
//成功返回0,失败返回-1

5.6 读和写流

对于打开的流,有三种非格式化I/O:

  1. 每次一个字符的I/O 。一次读写一个字符,如果流带缓冲,标准I/O处理所有缓冲
  2. 每次一行的I/O。使用fgets和fputs,每行都以一个换行符终止。
  3. 直接I/O。fread和fwrite函数支持这种类型的I/O,每次I/O读取某种数量的对象,比如从二进制文件中每次读写一个结构

输入函数:

1
2
3
4
5
//以下3个函数用于一次读一个字符
int getc(FILE *fp);
int fgetc(FILE *fp);
int getchar(void);
//成功返回下一个字符,到达文件尾或出错返回EOF

getchar等同于getc(stdin)。getc和fgetc的区别,getc可以被实现成宏(c写的少不太了解,意思貌似是getc的实现可能完全是宏实现的,不是一个函数,而fgetc一定是函数)

由于到达文件尾和出错都返回EOF所以需要一组函数来判断到底是哪种情况

1
2
3
4
5
#include <stdio.h>
int ferror(FILE *fp);
int feof(FILE *fp);
//两个函数的返回值,为真返回非0,否则返回0
void clearerr(FILE *fp);

大多数实现,流会在FILE对象中维护两个标志:

  1. 出错标志
  2. 文件结束标志

调用clearerr可以清除这两个标志

1
2
3
//从流中读取数据以后,再将其压送回流中
int ungetc(int c, FILE *fp);
//成功返回c,失败返回EOF

cpp primer标准I/O也提过这函数,不过没实际用过这东西,大概运用场景就是,要借助下一个字符来进行判断的时候,不用这函数的话,就需要单独再设置一个临时变量

输出函数:

1
2
3
4
5
#include <stdio.h>
int putc(int c, FILE *fp);
int fputc(int c, FILE *fp);
int putchar(int c);
//成功返回c,失败返回EOF

与前面输入的区别类似

5.7 每次一行I/O

以下两个函数提供每次输入一行的功能

1
2
3
4
#include <stdio.h>
char *fgets(char *restrict buf, int n, FILE *restrict fp);
char *gets(char *buf);
//成功返回buf,到达文件尾或出错返回NULL

gets从标准输入读,fgets从指定流读,fgets需要指定长度,且对于fgets必须指定缓冲区长度,且每次读的字符数不超过n-1,否则读的是不完整的行,最后一个字符以NULL字节结尾。gets函数不推荐使用,不能指定缓冲区长度,存在缓冲区溢出问题,另一个区别gets不将换行符存入缓冲区

1
2
3
4
//fputs和puts提供输出一行的功能
int fputs(const char *restrict str, FILE *restrict fp);
int puts(const char *str);
//成功返回非负值,出错返回EOF

区别puts会在每次输出后添加一个换行符?

5.8 标准I/O的效率

对同一文件,采取不同的I/O函数,结论是,在系统cpu上占用的时间基本一样,时间差别主要在用户cpu上,由于标准I/O中读的时候存在的循环比read长很多(搞不懂这个循环是干啥的,一亿次的循环就离谱),差别主要也是由于这个导致的

image-20221020232321872

5.9 二进制I/O

读写一个完整结构的时候通常用到二进制I/O操作,因为这种情况下,其他的I/O操作处理起来会非常麻烦,例如收到null或换行符的影响直接停止了,不能正确工作

1
2
3
4
#include <stdio.h>
size_t fread(void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp);
size_t fwrite(const void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp);
//两个函数的返回值,读写的对象数

常见用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//1)读写一个二进制数组。例如,将一个浮点数组的第2-5个元素写入文件
float data[10];
if (fwrite(&data[2], sizeof(float), 4, fp) != 4)
err_sys("fwrite error");

//2)读写一个结构。例如
struct {
short count;
long total;
char name[NAMESIZE];
} item;

if (fwrite(&item, sizeof(item), 1, fp) != 1)
err_sys("fwrite error");

二进制I/O的问题,只能用于同一系统上已写的数据,另一个系统可能由于以下原因不能正常工作:

  1. 在一个结构中,同一成员的偏移量可能随编译程序和系统的不同而不同
  2. 用来存储多字节整数和浮点值的二进制格式在不同系统结构下也可能不同

5.10 定位流

三组不同的,区别不大主要在于偏移量的数据类型的不同

1
2
3
4
5
6
#include <stdio.h>
long ftell(FILE *fp);
//成功返回当前文件位置指示,出错返回-1
int fseek(FILE *fp, long offset, int whence);
//成功返回0,失败返回-1
void rewind(FILE *fp);

对于二进制文件以字节为单位度量,whence参数与lseek一样,SEEK_SET,SEEK_CUR,SEEK_END。对于文本文件,由于可能存在不同的格式来存放,所以当前位置不能以简单的字节偏移量来度量,所以要定位文本文件,whence一定要是SEEK_SET,offset只能有两种值,0或ftell返回的值。使用rewind函数设置到起始位置。

1
2
3
4
5
6
7
8
off_t ftello(FILE *fp);
//成功返回当前文件位置,失败返回(off_t) -1
int fseeko(FILE *fp, off_t offset, int whence);
//成功返回0,失败返回-1

int fgetpos(FILE *restrict fp, fpos_t *restirct pos);
int fsetpos(FILE *fp, const fpos_t *pos);
//成功返回0,失败返回非0

5.11 格式化I/O

格式化输出:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int printf(const char *restrict format, ...);
int fprintf(FILE *restrict fp, const char *restrict format, ...);
int dprintf(int fd, const char *restrict format, ...);
//3个函数成功返回字符输出数,失败返回负值
int sprintf(char *restrict buf, const char *restrict format, ...);
//成功返回存入数组字符数,编码出错返回负值
int snprintf(char *restrict buf, size_t n, const char *restrict format, ...);
//若缓冲区够大,返回存入数组的字符数,编码出错返回负值

printf向标准输出,fprintf向指定流,dprintf向指定文件,sprintf将格式化字符送入数组(就类似于字符串拼接之后再输出),snprintf就是指定明确长度缓冲区的sprintf防止缓冲区溢出

格式化字符串: %[flags] [fldwidth] [precision] [lenmodifier] convtype

flags:各种标志 fldwidth:最小字段宽度 precision:小数点后最小位数 lenmodifier:参数长度 convtype:必选的,它控制如何解释参数 ,这一页有各种详细的参数,起始没啥注意的,就平时写的那种%3.2d这种,只不过更详细,有需求的时候看一下 P128

image-20221021002541816

image-20221021002557774

image-20221021002607999

1
2
3
4
5
6
7
8
9
10
11
//五种类似于上面的变体,可变参数... 替换成了arg
#include <stdarg.h>
#incldue <stdio.h>
int vprintf(const char *restrict format, va_list arg);
int vfprintf(FILE *restrict fp, const char *restrict format, va_list arg);
int vdprintf(int fd, const char *restrict format, va_list arg);
//成功返回输出字符数,出错返回负值
int vsprintf(char *restrict buf, const char *restrict format, va_list arg);
//成功返回存入数组的字符数,编码出错返回负值
itn vsprintf(char *restrict buf, size_t n, const char *restrict format, va_list arg);
//缓冲区够大返回存入数组字符数,编码出错返回负值

格式化输入:

1
2
3
4
5
#include <stdio.h>
int scanf(const char *restirct format, ...);
int fscanf(FILE *restrict fp, const char *restrict format, ...);
int sscanf(const char *restrict buf, const char *restrict format, ...);
//3个函数的返回值,赋值的输入项数,出错或到文件尾端返回EOF

和前面类似: %[*] [fldwidth] [m] [lenmodifier] convtype

用到的时候再细看,P130页

image-20221021003306627

1
2
3
4
5
6
//一样也有一组,变体
#include <stdarg.h>
#include <stdio.h>
int vscanf(const char *restrict format, va_list arg);
int vfscanf(FILE *restrict fp, const char *restrict format, va_list arg);
int vsscanf(const char *restrict buf, const char *restrict )

5.12 实现细节

标准I/O库最终都要调用第3章中说明的I/O例程。每个标准I/O流都有一个与其相关联的文件描述符,可以对流调用fileno获取

1
2
3
#include <stdio.h>
int fileno(FILE *fp);
//返回与流关联的文件描述符

可以查看stdio.h源文件,观察实现

5.13 临时文件

IOS C 标准I/O库提供两个函数来帮助创建临时文件

1
2
3
4
5
#include <stdio.h>
char *tmpnam(char *ptr);
//返回指向唯一路径名的指针
FILE* tmpfile(void);
//成功返回文件指针,出错返回NULL

tmpnam产生一个与现有文件名不同的有效路径字符串,ptr是NULL时,产生的路径名放在一个静态区中,指向该静态区的指针作为函数值返回,若ptr不是NULL,则认为它指向的是一个长度至少是L_tmpnam的字符数组

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>
int main() {
char name[L_tmpnam], line[MAXLINE];
FILE *fp;

printf("%s\n", tmpnam(NULL));

tmpnam(name);
printf("%s\n", name);

if ((fp = tmpfile()) == NULL)
err_sys("tmpfile error");
fputs("one line of output:", fp);
rewind(fp);
if (fgets(line, sizeof(line), fp) == NULL)
err_sys("fgets error");
fputs(line, stdout);
exit(0);
}

//运行结果
$ ./a.out
/tmp/fileT0Hsu6
/tmp/filekmAsYQ
one line of output:

mkdtemp和mkstemp函数

1
2
3
4
5
#include <stdlib.h>
char *mkdtemp(char *template);
//成功返回指向目录名的指针,出错返回NULL
int mkstemp(char *template);
//成功返回文件描述符,出错返回-1

mkdtemp创建目录,mkstemp创建文件,名字通过template字符串进行选择,这个字符串后六位设置为xxxxxx,函数会将这些占位符替换成唯一且有效的,mkdtemp创建的权限:S_IRUSR | S_IWUSR | S_IXUSR,可以通过掩码来影响;mkstemp创建的权限:S_IRUSR,S_IWUSR,mkstemp创建的临时文件不会自动删除,需要手动unlink,与tempfile相比的优点是,获取文件名和创建文件之间没有时间间隔,做到了类似原子操作的功效,具体是不是没说

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
//示例
#include <stdlib.h>
#include <errno.h>

void make_temp(char* template);

int main() {
char good_template[] = "/tmp/dirXXXXXX"; //right way
char* bad_template = "/tmp/dirXXXXXX"; //wrong way

printf("trying to create first temp file...\n");
make_temp(good_template);
printf("trying to create second temp file...\n");
make_temp(bad_template);

exit(0);
}

void make_temp(char* template) {
int fd;
struct stat sbuf;

if ((fd = mkstemp(template)) < 0)
err_sys("mkstemp error");
printf("temp name = %s\n", template);
close(fd);

if (stat(template, &sbuf) < 0) {
if (errno == ENOENT)
printf("file dosen't exist\n");
else
err_sys("stat failed");
} else {
printf("file exists\n");
unlink(template);
}
}

5.14 内存流

标准I/O库把数据缓存在内存中,通过调用setbuf和setvbuf函数可以让I/O库使用我们自己的缓冲区。

内存流的创建

1
2
3
#include <stdio.h>
FILE* fmemopen(void* restrict buf, size_t size, const char* restrict type);
//成功返回流指针,错误返回NULL

image-20221021163311747

与标准I/O流有一定的区别:

  1. 无论何时以追加写方式打开内存流,当前文件位置设为缓冲区的第一个null字节,如果缓冲区中不存在null字节,则当前位置就设为缓冲区结尾的后一个字节。当流不是以追加写方式打开,当前位置设置为缓冲区的开始位置。
  2. 如果buf参数是一个NULL指针,读写都没有意义,因为没有办法找到缓冲区的位置,就无法读取已写入的内容
  3. 任何时候需要增加流缓冲区中的数据量以及调用fclose、fflush、fseek、fseeko和fsetpos时都会在当前位置写入一个null字节
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
#include <stdio.h>
#define BSZ 48

int main() {
FILE* fp;
char buf[BSZ];

memset(buf, 'a', BSZ-2);
buf[BSZ-2] = '\0';
buf[BSZ-1] = 'X';

if ((fp = fmemopen(buf, BSZ, "w+")) == NULL)
err_sys("fmemopen failed");
printf("initial buffer contents: %s\n", buf);
fprintf(fp, "hello, world");
printf("before flush: %s\n", buf);
fflush(fp);
printf("after fflush: %s\n", buf);
printf("len of string in buf = %ld\n",(long)strlen(buf));

memset(buf, 'b', BSZ-2);
buf[BSZ-2] = '\0';
buf[BSZ-1] = 'X';
fprintf(fp, "hello, world");
fseek(fp, 0, SEEK_SET);
printf("after fseek: %s\n", buf);
printf("len of string in buf = %ld\n", (long)strlen(buf));

memset(buf, 'c', BSZ-2);
buf[BSZ-2] = '\0';
buf[BSZ-1] = 'X';
fprintf(fp, "hello, world");
fclose(fp);
printf("after fclose: %s\n", buf);
printf("len of string in buf = %ld\n", (long)strlen(buf));

return 0;
}
//运行结果
p139
简单来说,就是通过I/O函数向一个自己的缓冲区写数据,而不是向文件里写了,当前位置的变化只会受到上面第3点里那几个函数的影响

另外两个创建内存流的函数

1
2
3
4
5
#include <stdio.h>
FILE* open_memstream(char** bufp, size_t* sizep);
#include <wchar.h>
FILE* open_wmemstream(wchar_t** bufp, size_t* sizep);
//成功返回流指针,出错返回NULL

与fmemopen的区别:

  1. 创建的流只能写打开
  2. 不能指定自己的缓冲区,但可以分别通过bufp和sizep参数访问地址和大小
  3. 关闭流后需自行释放缓冲区
  4. 对流添加字节会增加缓冲区大小

用法感觉会想到stringstream是不是类似实现的

5.15 标准I/O的替代软件

p140, 提了一些替代软件,有需要再看吧,暂时应该很难用到这么深入的I/O操作

5.16 小结

本章就深入的介绍了I/O库的细节,缓冲技术的细节

第7章 进程环境

7.1 引言

下一章开始进程控制,这一章介绍一些前置知识,以及程序如何运行的

7.2 main函数

1
2
//mian函数的原型
int main(int argc, char* argv[]);

内核执行C程序时(使用一个exec函数),在调用main前先调用一个特殊的启动例程。可执行程序文件将此启动例程指定为程序的起始地址(创建虚拟内存?)——-这是由连接编辑器设置的,而连接编辑器则由C编译器调用。启动例程从内核取得命令行参数和环境变量值,然后按上述方式调用main函数做好安排

7.3 进程终止

有8种方式使进程终止,其中5种为正常终止:

  1. 从main返回
  2. 调用exit
  3. 调用_exit或 _Exit
  4. 最后一个线程从其启动例程返回
  5. 从最后一个线程调用pthread_exit

异常终止有3种方式:

  1. 调用abort
  2. 接到一个信号
  3. 最后一个线程对取消请求做出响应

退出函数:

exit和_Exit立即进入内核,exit先执行一些清理工作

1
2
3
4
5
#include <stdlib.h>
void exit(int status);
void _Exit(int status);
#include <unistd.h>
void _exit(int status);

函数atexit:

1
2
3
#include <stdlib.h>
int atexit(void (*func)(void));
//成功返回0,出错返回非0

ISOC规定一个进程可以登记至多32个函数,这些函数由exit自动调用,称这些为进程终止函数,调用atexit函数来登记这些函数

image-20221023153958629

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
//示例
#include <stdlib.h>
#include <unistd.h>

static void my_exit1(void);
static void my_exit2(void);

int main() {
if (atexit(my_exit2) != 0)
err_sys("can't register my_exit2");

if (atexit(my_exit1) != 0)
err_sys("can't register my_exit1");
if (atexit(my_exit1) != 0)
err_sys("can't register my_exit1");

printf("main is done\n");
}

static void my_exit1(void) {
printf("firstt exit handler\n");
}

static void my_exit2(void) {
printf("second exit handler\n");
}

//执行结果
$ ./a,out
main is done
first exit handler
first exit handler
seconde exit handler

7.4 命令行参数

执行一个程序的时候,调用exec的进程可将命令行参数传递给新程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//示例   假设可执行程序名为echoarg
#include <stdio.h>

int main(int argc, char* argv[]) {
int i;
for (i = 0; i < argc; i++)
printf("argv[%d]: %s\n", i, argv[i]);
exit(0);
}

//执行结果
$ ./echoarg arg1 TEST foo
argv[0]: ./echoarg
argv[1]: arg1
argv[2]: TEST
argv[3]: foo

7.5 环境表

每个程序会接收到一张环境表,与参数表一样,同样是字符指针数组

1
extern char** environ;

环境字符串的一般命名惯例:

1
2
3
/* name=value 并且以null结尾*/
HOME=/home/sar\0
PATH=:/bin:/bash\0

image-20221023155150712

7.6 C程序的存储空间布局

正文段:

CPU执行的机器指令部分。通常,正文段是可共享的,且只读,防止程序意外修改

初始化数据段:

包含了程序种需明确地赋初值的变量。例如 int maxcount = 99(任何函数之外的声明);

未初始化数据段:

通常被称为bss段,例如 long sum[1000];程序开始执行前,将此段中的数据初始化为0或空指针

栈:

自动变量,以及每次函数调用时所需保存的信息存放在此段。每次函数调用时,其返回地址以及调用者的环境信息。最近被调用的函数在栈上为其自动和临时变量分配存储空间

堆:

通常在堆中进行动态存储分配,一般位于栈和未初始化数据段之间

image-20221023155904977

这个图只是一个比较简单的,实际上还存在一些段,动态库那些,视频笔记部分的虚拟内存图比较完整

7.7 共享库

应该就是动态库,减少可执行文件大小,可以查看上面视频部分笔记

7.8 存储空间分配

1
2
3
4
5
6
#inlcude <stdlib.h>
void* malloc(size_t size);
void* calloc(size_t nobj, size_t size);
void* realloc(void* ptr, size_t newsize);
//成功返回非空指针,出错返回NULL
void free(void* ptr);

基本就是熟悉的那一套

替代的存储空间分配程序:

  1. libmalloc
  2. vmalloc
  3. quick_fit
  4. jemalloc
  5. TCMalloc
  6. alloca

以后学项目的时候遇到再看吧,常规的目前完全够用了

7.9 环境变量

1
2
3
#include <stdlib.h>
char* getenv(const char* name);
//返回指向与name关联的value指针,若未周到,返回NULL

SingleUnixSpecification定义的环境变量:

image-20221023170134190

设置环境变量的三个函数: (只能影响当前进程及其生成和调用的子进程,不能影响父进程的环境)

1
2
3
4
5
6
#include <stdlib.h>
int putenv(char* str);
//成功返回0,出错返回非0
int setenv(const char* name, const char* value, int rewrite);
int unsetenv(const char* name);
//成功返回0,出错返回-1

3函数的操作如下:

  1. putenv取形式为name=value的字符串,将其放入环境表中,如果name已经存在,删除其原来的定义
  2. setenv将name设置为value。如果name已经存在,那么(a) 若rewrite非0,则首先删除其现有的定义;(b) 若rewrite为0,则不删除现有定义,不设置新的value也不出错
  3. unsetenv删除name的定义,即使不存在也不出错

7.10 函数setjmp和longjmp

longjmp的两个参数,第一个参数保存返回栈帧的状态,第二个是从set返回的返回值

这两个函数的作用类似于goto,但goto语句不能跨函数执行,这个可以。书中给出了这么一个场景

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
#include "apue.h"
#define TOK_ADD 5

void do_line(char*);
void cmd_add(void);
int get_token(void);

int main() {
char line[MAXLINE];

while (fgets(line, MAXLINE, stdin) != NULL)
do_line(line);

exit(0);
}

char* tok_ptr;

void do_line(char* ptr) {
int cmd;

tok_ptr = ptr;
while ((cmd = get_token()) > 0) {
switch(cmd) {
case TOK_ADD:
cmd_add();
break;
}
}
}

void cmd_add(void) {
itn token;

token = get_token();
}

int get_token(void) {
....
}

程序在运行的时候会在栈里面不断创建栈帧

image-20221023172616357

如果在cmd_add中出错的话,我们希望它立刻返回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
#include <setjmp.h>
int setjmp(jmp_buf env);
//直接调用返回0,从longjmp返回非0

//使用示例
#include "apue.h"
#include <setjmp.h>
#define TOK_ADD 5

jmp_buf jmpbuffer;

int main() {
char line[MAXLINE];

//返回的位置
if (setjmp(jmpbuffer) != 0)
printf("error");
while (fgets(line, MAXLINE, stdin) != NULL)
do_line(line);
exit(0);
}
.
.
.
void cmd_add() {
int token;
token = get_token();
if (token < 0) /* an error has occured */
longjmp(jmpbuffer, 1);
}

自动变量,寄存器变量和易失变量(volatile):

简单来说,虽然恢复到了调用之前的栈帧,但也不是完全恢复调用前的状态,在不开优化的情况下,被改变的变量不会恢复,开优化的情况下,自动变量和寄存器变量存放在寄存器中,能得到恢复

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
#include "apue.h"
#include <setjmp.h>

static void f1(int,int,int,int);
static void f2(void);

static jmp_buf jmpbuffer;
static int global;

int main() {
int autoval;
register int regival;
volatile int volaval;
static int statval;

global = 1, autoval = 2, regival = 3, volaval = 4, statval = 5;

if (setjmp(jmpbuffer) != 0) {
printf("after longjmp\n");
printf("global = %d, autoval = %d, rigival = %d,"
" volaval = %d, statval = %d\n",global, autoval, regival,
volaval, statval);
exit(0);
}

global = 95, autoval = 96, regival = 97, volaval = 98, statval = 99;

f1(/*参数*/);
}

static void f1(int i, int j, int k, int l) {
/* 打印longjmp前的参数 */

f2();
}

static void f2(void) {
longjmp(jmpbuffer, 1);
}

//执行结果
//不开优化
gcc testjmp.c
./a.out
in f1():
95 96 97 98 99 /* global autoval regival volaval statval*/
after longjmp:
96 96 97 98 99
gcc -O testjmp.c
./a.out
in f1():
95 96 97 98 99
after longjmp:
95 2 3 98 99

自动变量的潜在问题:

这里举得例子就是,在函数中打开了一个流,并且在函数内创建了个局部变量,作为流的缓冲区,函数返回时,它在栈上使用的空间会被下一个函数调用使用,但这个流仍然使用这部分存储空间作为流的缓冲区

7.11 函数getrlimit和setrlimit

每个进程都有一组资源限制,其中一些可以用getrlimit和setrlimit查询和更改

1
2
3
4
5
6
7
8
#include <sys/resource.h>
int getrlimit(int resource, struct rlimit *rlptr);
int setrlimit(int resource, const struct rlimit *rlptr);
//成功返回0,出错返回非0
struct rlimit {
rlim_t rlim_cur; /* sofr limit: current limit */
rlim_t rlim_max; /* hard limit: maximum value for rlim_cur */
}

进程的资源线程一般由0进程创建,然后由后续进程继承

更改资源限制时,必须遵循下列3条规则:

  1. 任何一个进程都可将一个软限制值更改为小于或等于其硬限制值
  2. 任何一个进程都可降低其硬限制值,但它必须大于或等于其软限制值,且这种降低对于普通进程是不可逆的
  3. 只有超级用户进程才可以提高硬限制值

然后就是一些具体的参数取值,以及代码示例

参数如下:

image-20221023175227794

RLIMIT_AS: 进程的总的可用存储空间的最大长度,影响到sbrk函数和mmap函数

RLIMIT_CORE: core文件的最大字节数,若值为0,阻止创建core文件

RLIMIT_CPU: CPU时间的最大量值,当超过此软限制,向进程发送SIGXCPU信号

RLIMIT_DATA: 数据段的最大字节长度,初始化数据、非初始化以及堆的综合

RLIMIT_FSIZE: 可以创建的文件最大字节长度,超过此软限制,发送SIGXFSZ信号

RLIMIT_MEMLOCK: 一个进程使用mlock能够锁定在存储空间中的最大字节长度

RLIMIT_MSGQUEUE: 进程为POSIX消息队列可分配的最大存储字节

RLIMIT_NICE: 为了影响进程的调度优先级。友好值的最大设置限制

RLIMIT_NOFILE: 每个进程能打开的最多文件数

……..还有一些P177

资源限制影响到调用进程并由其子进程继承,所以为了影响一个用户的后续进程,需将资源限制的设置构造在shall中

7.12 小结

一些进程有关的基础内容,以及C程序的实际布局,程序怎么终止的,命令行模式下接触的多的命令行参数,比goto更强的可在函数中跳出的setjmp和longjmp,以及资源限制

第8章 进程控制

8.1 引言

进程相关控制,机制,属性

8.2 进程标识

每个进程运行的时候都会有一个唯一的进程ID,但进程终止之后,这个ID能被其他进程复用,当然这个复用有一个延迟机制,不会复用最近终止的进程,同时也举了一些特殊的进程ID,如0,1,2进程,交换进程,init进程,页守护进程,这些进程由系统专用,满足系统的一些需求,对于init进程还有个特殊的地方,它会成为所有孤儿进程的父进程

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <unistd.h>
pid_t getpid(void);
//返回调用进程的进程ID
pid_t getppid(void);
//返回调用进程的父进程ID
uid_t getuid(void);
//返回进程的实际用户ID
uid_t geteuid(void);
//返回进程的有效用户ID
gid_t getgid(void);
//返回进程的实际组ID
gid_t getdgid(void);
//返回进程的有效组ID

8.3 函数fork

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
#include <unistd.h>
pid_t fork(void);
//父进程返回子进程ID,子进程返回0 出错返回-1

//示例
#include <unistd.h>

int globvar = 6; /* external variable in initialized data */
char buf[] = "a write to stdout\n";

int main() {
int var; /*automatic variable on the stack */
pid_t pid;

var = 88;
if (write(STDOUT_FILENO, buf, sizeof(buf) -1) != sizeof(buf)-1)
err_sys("write error");
printf("before fork\n"); /* we don't flush stdout */

if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0) { /* child */
globvar++;
var++;
} else {
sleep(2); /* father */
}

printf("pid = %ld, glob = %d, var = %d\n", (long)getpid(), globvar, var);

exit(0);
}
//运行结果
$ ./a.out
a write to stdout
before fork
pid = 430, glob = 7, var = 89 子进程的值改变了
pid = 429, glob = 6, var = 88 父进程的值不变
$ ./a.out > temp.out
$ cat temp.out
a write to stdout
before fork
pid = 430, glob = 7, var = 89
before fork
pid = 429, glob = 6, var = 88

对于fork完后,先执行子进程还是父进程是不确定的,所以用sleep保证先执行子进程,对于两次执行结果,第一次直接对标准输出写,第二次将标准输出重定向到temp.out可以发现,此时before fork变成两行了,问题在于,write是不缓冲的,而对于标准库的I/O,是带缓冲的,如果连接到设备终端是行缓冲,但重定向到文件之后,它是全缓冲的,缓冲区随着fork一起被复制了,在子进程结束时被写到相应文件

文件共享:

简单来说就是父子进程的文件描述符表中的文件描述符指向同一文件表项,就像执行了dup函数

image-20221024154956178

fork之后处理文件描述符表有以下两种常见的情况:

  1. 父进程等待子进程完成。
  2. 父进程和子进程各自执行不同的程序段。各自关闭它们不需要的文件描述符

除打开文件之外,其他子进程从父进程继承的属性:

  1. 实际用户ID,实际组ID,有效用户ID,有效组ID
  2. 附属组ID
  3. 进程组ID
  4. 会话ID
  5. 控制终端
  6. 设置用户ID标志的和设置组ID标志
  7. 当前工作目录
  8. 根目录
  9. 文件模式创建屏蔽字
  10. 信号屏蔽和安排
  11. 对任一打开文件描述符的执行时关闭标志
  12. 环境
  13. 连接的共享存储段
  14. 存储映像
  15. 资源限制

父子进程的区别如下:

  1. fork的返回值不同
  2. 进程ID不同
  3. 子进程的tms_utime,tms_stime,tms_cutime和tms_ustime的值设置为0
  4. 子进程不继承父进程设置的文件锁
  5. 子进程的未处理闹钟被清除
  6. 子进程的未处理信号集被设置为空集

对于fork的一种常见用法时,fork之后子进程执行exec,spawn将这两个操作组合成一个操作

8.4 函数vfork

函数用法上和fork相同,但语义不同。vfork的目的是,创建一个新进程,新进程的目的是执行一个新程序,所以它与fork的一个区别是,vfork保证子进程优先运行,在它调用exec或exit之后,父进程才可能被调度

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
//试一下 vfork
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int globval = 88;

int main() {

pid_t pid;

if ((pid = vfork()) < 0) {
perror("vfork error");
} else if (pid == 0) {
globval++;
}
printf("pid = %ld, glob = %d\n", (long)getpid(), globval);
sleep(5);
exit(0);
}

//结果
pid = 714, glob = 89
pid = 713, glob = 89

globval父子进程都一样,这是vfork的另一个区别,完全不复制,地址空间用的就是父进程的

8.5 函数exit

7.3节曾说过,进程有5种正常终止以及3种异常终止方式。

5种正常终止方式具体如下:

  1. 在main种调用return函数,相当于exit
  2. 调用exit函数,exit函数具体作用在 7.3 中提过,但对于UNIX系统不完整,因为它不处理文件描述符,多进程以及作业控制
  3. 调用_exit或 _Exit函数。其目的是为进程提供一种无需运行终止处理程序或信号处理程序而终止的方法,对标准I/O流是否冲洗,取决于实现。
  4. 进程的最后一个线程在其启动例程中执行return语句。
  5. 进程的最后一个线程调用pthread_exit函数。

3种异常终止具体如下:

  1. 调用abort。它产生SIGABRT信号,这是下一种异常终止的一种特例。
  2. 当进程接收到某些信号时。(第10章会较详细地说明信号)
  3. 最后一个线程对 取消 请求作出相应。

不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应的进程关闭所有打开描述符,释放它所使用的存储器。

这里也提及了以下僵尸进程和孤儿进程,僵尸进程产生于,子进程结束了但父进程没有对其进行wait处理,孤儿进程产生于,父进程先于子进程结束,这些子进程会称为init进程的子进程,被其处理

8.6 函数wait和waitpid

当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。可以注册信号捕捉函数来处理它,这种信号的默认处理是忽略。

调用wait或waitpid时可能发生的事情:

  1. 如果其所有子进程都还在运行,则阻塞
  2. 如果一个子进程已终止,正等待父进程获取其终止状态,则取得该进程的终止状态立即返回
  3. 如果它没有任何子进程,则立即返回出错
1
2
3
4
#include <sys/wait.h>
pid_t wait(int* statloc);
pid_t waitpid(pid_t pid, int* statloc, int options);
//成功返回进程ID,出错返回0(见后面说明)或-1

两函数区别:

  1. 在一个子进程终止前,wait使其调用者阻塞吗,而waitpid有一选项,可使得调用者不阻塞
  2. waitpid并不等待在其调用之后的第一个终止子进程,它有若干选项,可以控制它等待的进程

statloc传出终止状态,配合特定的宏使用: (上面视频笔记部分的wati就有)

image-20221024164930870

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
//示例
#include <sys/wait.h>

void pr_exit(int status) {
if (WIFEXITED(status))
printf("normal termination, exit status = %d\n",
WEXITSTATUS(status));
else if (WIFSIGNALED(status))
printf("abnormal termination, signal number = %d%s\n",
WTERMSIG(status),
#ifdef WCOREDUMP
WCOREDUMP(status) ? " (core file generated)" : "");
#else
"");
#endif
else if (WIFSTOPPED(status))
printf("child stopped, signal number = %d\n",
WSTOPSIG(status));
}

//调用上述程序的例子
#include <sys/wait.h>
int main() {
pid_t pid;
int status;

if ((pid = fork()) < 0)
err_sys("fork error");
else if (pid == 0) /* child */
exit(7);

if (wait(&status) != pid) /* wait fo child */
err_sys("wait error");
pr_exit(status); /* and print its status */

if ((pid = fork()) < 0)
err_sys("fork error"); /* child */
else if (pid == 0)
abort(); /* generates SIGABRT */

if (wait(&status) != pid) /* wait for child */
err_sys("wait error");
pr_exit(status); /* and print its status */

if ((pid = fork()) < 0)
err_sys("fork error");
else if (pid == 0) /* child */
status /= 0; /* 除0错误,产生一个信号 */

if (wait(&status) != pid) /* wait for child */
err_sys("wait error");
pr_exit(status); /* and print its status */

exit(0);
}

waitpid的参数作用如下:

  1. pid == -1 ,等待任一子进程,此时waitpid与wait等效
  2. pid > 0 ,等待进程ID与pid相等的子进程
  3. pid == 0 ,等待组ID等于调用进程组ID的任一子进程
  4. pid < -1 ,等待组ID等于pid绝对值的任一子进程

options参数如下: 此参数或是0,或是如下常量

image-20221024195636184

8.7 函数waitid

此函数类似于waitpid,但更灵活

1
2
3
#include <sys/wait.h>
int waitid(idtype_t idtype, id_t id, siginfo_t* infop, int options);
//成功返回0,出错返回-1

image-20221024200426117

8.8 函数wait3和wait4

多一个参数,该参数允许内核返回由终止进程及其子进程使用的资源概况

1
2
3
4
5
6
7
8
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>

pid_t wait3(int* statloc, int options, struct rusage* rusage);
pid_t wait4(pid_t pid, int* statloc, int options, struct rusage* rusage);
//成功返回进程ID,出错返回-1

资源统计信息,包括用户CPU时间总量,系统CPU时间总量,缺页次数,接收到信号的次数等。

image-20221024200812484

8.9 竞争条件

就是上述视频笔记部分的时态竞争,由于不确定进程谁会优先执行,产生的问题

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
//示例
#include <apue.h> /* 假设apue里面包含了所有所需的头文件 */

int main() {
pid_t pid;

if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0) {
charatatime("output from child\n");
} else {
charatatime("output from parent\n");
}

exit(0);
}

static void charatatime(char* str) {
char* ptr;
int c;

setbuf(stdout, NULL);
for (ptr=str; (c=*ptr++); ) {
putc(c, stdout);
}
}

//上述程序就存在竞争,父子进程谁先输出不确定
//改进示例
#include "apue.h"

int main() {
pid_t pid;

TELL_WAIT();

if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0) {
WAIT_PARENT(); /* parents go first */
charatatime("output from child\n");
} else {
charatatime("output from parent\n");
TELL_CHILD(pid);
}
exit(0);
}

static void charatatime(char* str) {
char* ptr;
int c;

setbuf(stdout, NULL);
for (ptr=str; (c=*ptr++); ) {
putc(c, stdout);
}
}

//具体wait怎么实现,可能是IPC,也可能是信号

8.10 函数exec

调用exec并不创建新进程,所以前后的进程ID并未改变,exec只是用磁盘上的一个新程序替换了当前进程的正文段,数据段,堆段和栈段。

1
2
3
4
5
6
7
8
9
#include <unistd.h>
int execl(const char* pathname, const char* arg0, ... /* (char*)0 */);
int execv(const char* pathname, char* const argv[]);
int execle(const char* pathname, const char* arg0, ... /* (char*)0, char* const envp[] */);
int execve(const char* pathname, char* const argv[], char* const envp[]);
int execlp(const char* filename, const char* arg0, ... /* (char*)0 */);
int execvp(const char* filename, char* const argv[]);
int fexecve(int fd, char* const argv[], char* const envp[]);
//出错返回-1,成功不返回

这些函数的第一个区别,前四个函数以路径名(pathname)作为参数,后两个取文件名(filename)作为参数,最后一个取文件描述符作为参数。当指定文件名为参数时:

  1. 如果filename中包含/,则将其视为路径
  2. 否则就按PATH环境变量,在它所指定的目录中搜寻可执行文件

image-20221025164348556

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
//示例
#include "apue.h"

char *env_init[] = {"USER=unknown","PATH=/tmp",NULL};

int main() {
pid_t pid;
if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0) {
if (execle("/home/sar/bin/echoall", "echoall", "myarg1", "MY arg2", (char*)0, env_init) < 0) {
ess_sys("execle error");
}
}

if (waitpid(pid, NULL, 0) < 0)
err_sys("wait error");

if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0) {
if (execlp("echoall", "echoall", "only 1 arg", (char*)0,) < 0)
err_sys("execlp error");
}

exit(0);
}

//exec执行的程序如下
#include "apue.h"

int main(int argc, char* argv[]) {
int i;
char** ptr;
extern char** environ;

for (i=0; i<argc; i++)
printf("argv[%d]: %s\n", i, argv[i]);

for (ptr = environ; *ptr != 0; ptr++)
printf("%s\n", *ptr);

exit(0);
}

8.11 更改用户ID和更改组ID

可以用setuid函数设置实际用户ID和有效用户ID,与此类似可以用setgid函数设置实际组ID和有效组ID

1
2
3
4
#include <unistd.h>
int setuid(uid_t uid);
int setgid(gid_t gid);
//成功返回0,出错返回-1

规则:如果几进程具有超级用户特权,设置的时候对实际用户ID,有效用户ID,保存的设置用户ID,都会被修改;反之,只会修改用户ID

更改3个ID位的另一个方法是执行exec函数的时候,exec会根据 设置用户ID位关闭与否 进行不同更改

  1. 设置用户ID位关闭时,实际用户ID不变,有效用户ID不变,保存的用户ID从有效用户ID复制
  2. 设置用户ID位打开时,实际用户ID不变,有效用户ID设置为程序文件的用户ID,保存的用户ID从有效复制

image-20221025170303562

函数setreudi和setregid

历史上,BSD支持setreuid函数,其功能是交换实际用户ID和有效用户IDimage-20221025170956259

1
2
3
4
#include <unistd.h>
int setreuid(uid_t ruid, uid_t euid);
int setregid(gid_t rgid, gid_t egid);
//成功返回0,出错-1

这种非常规的函数,还是真遇到了再细看

函数seteuid和setegid

类似于setuid和setgid但只会更改有效用户ID和有效组ID

1
2
3
4
#include <unistd.h>
int seteuid(uid_t uid);
int setegid(gid_t gid);
//成功返回0,出错-1

image-20221025170958630

可以看出setreuid是比较奇怪的,遇到再细看

组ID

本章说的一切函数适用于各个组ID,附属组除外

8.12 解释器文件

没看懂只能说,不知道干嘛用的,也没用过这东西,反之用来执行不同的脚本文件的,提高可移植性?以及提高效率,根据后面一节可能会更好理解,是shell能处理的命令更广泛?

8.13 函数system

在函数中使用命令来执行操作

1
2
3
#include <stdlib.h>
int system(const char* cmdstring);
//返回值见下

因为system在它的实现中调用了fork,exec和waitpid,因此有3种返回值:

  1. fork失败或者waitpid返回除EINTR之外的出错,返回-1,并且设置errno
  2. 如果exec失败,则其返回值如同shell执行了exit(127)
  3. 否则所有3个函数都成功,那么system的返回值,是shell的终止状态(?) waitpid那个终止状态好像是
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
//system的一种实现,它不对信号处理,后续会修改
#include <sys/wait.h>
#include <errno.h>
#include <unistd.h>

int system(const char* cmdstring) {
pid_t pid;
int status;

if (cmdstring == NULL)
return 1;

if ((pid = fork()) < 0) {
status = -1;
} else if (pid == 0) {
execl("/bin/sh", "sh", "-c", cmdstring, (char*)0);
_exit(127);
} else {
while (waitpid(pid, &status, 0) < 0) {
if (errno != EINTR) {
status = -1;
break;
}
}
}
return(status);
}

//测试用例
#include "apue.h"
#include <sys/wait.h>

int main() {
int status;

if ((status = system("date")) < 0)
err_sys("system() error");

//前面waitpid提过的,打印终止状态
pr_exit(status);

if ((status = system("nosuchcommand")) < 0)
err_sys("system() error");

pr_exit(status);

if ((status = system("who; exit 44")) < 0)
err_sys("system() error")

pr_exit(status);

exit(0);
}

//运行结果
$ ./a.out
Sat Feb 25 19:36:59 EST 2012
normal termination, exit status = 0 //对于date
sh: nosuchcomman: command not found
normal termination, exit status = 127 //对于无此种命令
sar console Jan 1 14:59
sar ttys000 Feb 7 19:08
sar ttys001 Jan 15 15:28
sar ttys002 Jan 15 21:50
sar ttys003 Jan 21 16:02
normal tremination, exit status = 44 //对于exit

还有个注意点,如果一个进程正以特殊的权限(设置用户ID或设置组ID)运行,它又想生成另一个进程执行另一个程序,则它应当直接使用fork和exec,而且在fork之后,exec之前,要更改会普通权限。设置用户ID或设置组ID程序,绝不应调用system函数(意思应该就是,因为system将fork和exec组合了,没办法在之间进行操作)

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
//示例
#include "apue.h"
int main(int argc, char* argv[]) {
int status;

if (argc < 2)
err_quit("comman-line argument required");

if ((status = system(argv[1])) < 0)
err_sys("system error");

pr_exit();

exit(0);
} /* 将次程序编译为可执行目标文件tsys */

#include "apue.h"
int main() {
printf("real uid = %d, effective uid = %d\n", getuid(), geteuid());
exit(0);
}/* 将次程序编译为可执行目标文件printuids */

//执行结果
$ tsys printuids
read uid = 205, effective uid = 205 /* 正常执行无特权 */
normal termination, exit status = 0
$su /* 成为超级用户并更改特权 */
Password:
# chown root tsys
# chmod u+s tsys
#ls -l tsys
-rwsrwxr-x 1 root 7788 Feb 25 22:13 tsys
# exit
$ tsys printuids
read uid = 205, effective uid = 0 /* 有效用户ID称为root */
normal termination, exit status = 0

8.14 进程会计

大多数UNIX系统提供一个选项以进行进程会计。启用该选项后,每当进程结束的时候,内核就会写一个记录。一般包括命令名,所使用的CPU时间总量,用户ID和组ID,启动时间等。

acct函数用来启用和禁用进程会计,唯一使用这一函数的是accton命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//会计记录结构
typedef u_short comp_t;/* 3-bit base 8 exponent; 13-bit fraction */
struct acct {
char ac_flag; /* flag (see Figure 8.26)
char ac_stat; /* termination status(signal & core flag only) */
/* (Solaris only)
uid_t ac_uid; /* read user ID */
gid_t ac_gid; /* read group ID */
dev_t ac_tty; /* controlling terminal */
time_t ac_btime; /* starting calendear time */
comp_t ac_utime; /* user CPU time */
comp_t ac_stime; /* system CPU time */
comp_t ac_etime; /* elapsed time */
comp_t ac_mem; /* average memory uage */
comp_t ac_io; /* bytes transferred (by read and write) */
/* "blocks" on BSD systems */
comp_t ac_rw; /* blocks read or written */
/* (not present on BSD systems) */
char ac_comm[8]; /* comman name: [8] for Solaris,*/
/* [10] for Mac OX X, [16] for FreeBSD, and [17] for linux */
}

8.15 用户标识

获取运行该程序的用户的登录名

1
2
3
#include <unistd.h>
char* getlogin(void);
//成功,返回指向登录名字符串的指针,出错返回NULL

8.16 进程调度

通过调整友好值,更改进程调度的优先级,友好值越小,优先级越高

1
2
3
#include <unistd.h>
int nice(int incr);
//成功返回新的友好值NZERO,出错返回-1

incr参数被增加到调用进程的友好值上。如果incr太大,系统直接把它降到最大合法值(友好值是有范围的,书上提到的是0 ~ (2*NZERO)-1,不过应该取决于实现) ,类似如果incr太小,系统把他提高到最小合法值

1
2
3
#include <sys/resource.h>
int getpriority(int which, id_t who);
//成功返回 -NZERO ~ NZERO-1 之间的友好值,出错返回-1

which参数以下三个直之一:

  1. PRIO_PROCESS表示进程
  2. PRIO_PGRP表示进程组
  3. PRIO_USER表示用户ID

如果who参数为0,表示调用进程,进程组或用户。当which设为PRIO_USR并且who为0,使用调用进程的实际用户ID。如果参数作用于多个进程,返回优先度最高的

1
2
#include <sys/resource.h>
int setpriority(int which, id_t who, int value);
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
//示例:
#include "apue.h"
#include <errno.h>
#include <sys/time.h>

#if defined(MACOS)
#include <sys/syslimits.h>
#elif defined(SOLARIS)
#include <limits.h>
#elif defined(BSD)
#include <sys/param.h>
#endif

unsigned long long count;
struct timeval end;

void checktime(char* str) {
struct timeval tv;

gettimeofday(&tv, NULL);
if (tv.tv_sec >= end.tv_sec && tv.tv_usec >= end.tv_usec) {
printf("%s count = %lld\n", str, count);
exit(0);
}
}

int main() {
pid_t pid;
char* s;
int nzero, ret;
int adj = 0;

setbuf(stdout, NULL);

#if defined(NZERO)
nzero = NZERO;
#elif defined(_SC_NZERO)
nzero = sysconf(_SC_NZERO);
#else
#error NZERO undefined
#endif

printf("NZERO = %d\n", nzero);
if (argc == 2)
adj = strtol(argv[1], NULL, 10);
gettimeofday(&end, NULL);
end.tv_sec += 10;

if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0) {
s = "child";
printf("current nice value int child is %d, adjusting by %d\n",
nice(0) + nzero, adj);
errno = 0;
if ((ret = nice(adj)) == -1 && errno != 0)
err_sys("child set scheduling priority");
printf("now child nice value is %d\n", ret + nzero);
} else {
s = "parent";
printf("current nice value in parent is %d\n", nice(0)+nzero);
}

for (;;) {
if (++count == 0)
err_quit("%s counter wrap", s);
checktime(s);
}
}

//运行结果
$ a.a.out
NZERO = 20
current nice value in parent is 20
current nice value in child is 20, adjusting by 0
now child nice value is 20
child count = 1859362
parent count = 1845338
$ ./a.out
NZERO = 20
current nice value in parent is 20
current nice value in child is 20, adjusting by 20
now child nice value is 39
parent count = 3595709
child count = 52111

当友好值相同时,父进程占有50.2%的CPU,子进程占用49.8%的CPU;当将子进程的友好值增加,即优先度降低,父进程占用98.5%的CPU,子进程只占用1.5%的CPU

8.17 进程时间

1.10节说明了我们可以度量的3个时间:墙上时钟时间,用户CPU时间和系统CPU时间。任一进程都可以调用times函数获得它自己以及已终止子进程的上述值

1
2
3
4
5
6
7
8
9
#include <sys/time.h>
clock_t times(struct tms* buf);
//成功返回流逝的墙上时间,出从返回-1
struct tms {
clock_t tms_utime; /* user CPU time */
clock_t tms_stime; /* system CPU time */
clock_t tms_cutime; /* user CPU time, terminated children */
clock_t tms_cstime; /* system CPU time, terminated children */
}
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
//示例
#include "apue.h"
#include <sys/times.h>

static void pr_times(clock_t, struct tms*, struct tms*);
static void do_cmd(char*);

int main(int argc, char* argv[]) {
int i;

setbuf(stdout, NULL);
for (i = 1; i < argc; i++)
do_cmd(argv[i]);
exit(0);
}

static void do_cmd(char* cmd) {
struct tms tmsstart, tmsend;
clock_t start, end;
int status;

printf("\ncommand: %s\n",cmd);

if ((start = times(&tmsstart)) == -1)
err_sys("times error");

if ((status = sytem(cmd)) < 0)
err_sys("system error");

if ((end = times(&tmsend)) == -1)
err_sys("times error");

pr_times(end - start, &tmsstart, &tmsend);
pr_exit(status);
}

static void pr_times(clock_t, struct tms*, struct tms*) {
static long clktck = 0;

if (clktck == 0)
if ((clktck = sysconf(_SC_CLK_TCK)) < 0)
er_sys("sysconf error");

printf(" real: %7.2f\n", real / (double) clktck);
printf(" user: %7.2f\n",
(tmsend->tms_utime - tmsstart->tms_utime) / (double) clktck);
.
.
.
printf(" child sys: %7.2f\n",
(tmsend->tms_cstime - tmsstart->tms_cstime) / (double) clktck);
}

image-20221025212837098

8.18 小结

比较熟悉的是之前视频部分看过的fork wait exec这些函数了,然后本小节,又详细的介绍了这些函数的一些变种,以及system函数的实际实现,进程会计对于进程再一次观察,以及对进程调度优先级的更改

第10章 信号

10.1 引言

信号是软件中断。信号提供了一种处理异步事件的方法。本章先对信号机制进行综述,说明每个信号的一般用法,然后分析早期实现的问题。

10.2 信号概念

产生信号的条件:

  1. 当用户按某些终端键,引发终端产生的信号。如ctrl+c通常产生中断信号(SIGINT)
  2. 硬件异常产生的信号。如除0操作,非法内存访问
  3. 进程调用kill函数可将任意信号发送给另一个进程或进程组。
  4. 用户可用kill命令将信号发送给其他进程
  5. 当检测到某种软件条件已经发生,并应将其通知有关进程时产生信号。如闹钟产生SIGALRM

信号的三种处理方式:

  1. 忽略此信号。
  2. 捕捉信号。
  3. 执行默认动作。

SIGKILL和SIGSTOP只执行默认动作,提供给系统一个有效的结束进程的方法

P252有各种信号的详细说明

10.3 函数signal

1
2
3
4
5
6
7
#include <signal.h>
void (*signal(int signo, void (*func)(int)))(int);
//成功返回以前的信号处理配置,出错返回SIG_ERR

//可读性高的写法
typedef void Sigfunc(int);
Sigfunc* signal(int, Sigfunc*);

signo参数是信号名。func的参数有三种。

  1. 如果指定SIG_IGN,表示忽略此信号
  2. 如果指定SIG_DFL,表示执行系统默认动作
  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
//示例
#include "apue.h"

static void sig_usr(int); /* one handler for both signals */

int main() {
if (signal(SIGUSR1, sig_usr) == SIG_ERR)
err_sys("can't catch SIGUSR1");
if (signal(SIGUSR2, sig_usr) == SIG_ERR)
err_sys("can't catch SIGUSR3");

for (;;)
pause;
}

static void sig_usr(int signo) {
if (signo == SIGUSR1)
printf("received SIGUSR1\n");
else if (signo == SIGUSR2)
printf("received SIGUSR2\n");
else
err_dump("received signal %d\n", signo);
}

//运行结果
$ ./a.out
[1] 7216 /* 查看进程ID */
$ kill -USR1 7216
received SIGUSR1
$ kill -USR2 7216
received SIGUSR2
$ kill 7216
[1+] Terminated ./a.out

进程exec之后,原捕捉函数无意义了,但fork之后,仍然有效,因为子进程在开始复制了父进程的内存映像。所以信号捕捉函数的地址在子进程中是有意义的

10.4 不可靠的信号

早期信号的一些问题,比如阻塞信号的能力当时不具备。现在我们知道通过阻塞信号集和未决信号集实现了。以及,在进程每次接到信号对其进行处理时,随机将信号动作重置为默认值

1
2
3
4
5
6
7
8
9
10
//一个经典实例
int sig_int(); /* my signal handling function */
...
signal(SIGINT, sig_int) /* establish handler */
...
sig_int() {
signal(SIGINT, sig_int);
/* reestablish handler for next time */
/* process the signal */
}

问题在于,如果在sig_int中的signal重建捕捉之前,又发生一次中断信号,第二个信号执行默认动作,导致后续的捕捉函数失效。另一个问题在于,没有办法阻止信号发生,你只能忽略它,但没法阻止,现在通过阻塞信号集实现了

10.5 中断的系统调用

早期UNIX的一个特性,如果进程在执行一个低速系统调用而阻塞期间捕捉到了一个信号,则该系统调用就被中断不再继续执行。该系统调用返回出错,其errno设置为EINTR。

低速系统调用:

  1. 如果某些类型的文件数据不存在(管道,终端设备,网络设备),则读操作可能使调用者永远阻塞
  2. 如果这些数据不能被相同类型文件立即接受,则写操作可能会使调用者永久阻塞
  3. 在某种条件发生之前打开某些类型文件,可能发生阻塞(例如打开一个终端设备,需要等待与之连接的调制解调器应答)
  4. pause函数和wait函数
  5. 某些ioctl操作
  6. 某些进程间通信函数(15章)

10.6 可重入函数

可重入函数,也被称为异步信号安全的。它保证在执行信号处理函数的时候,不会对影响进程的执行。比如如果进程中正在malloc,此时信号产生,去执行信号处理函数,信号处理函数也malloc的话,通常malloc为它分配的存储区维护一个链表,信号处理函数可能更改此链表,由此可能对进程造成破坏

image-20221027121914840

不可重入函数的一些特征:

  1. 它们使用静态数据结构
  2. 它们调用malloc或free
  3. 它们是标准I/O函数,标准I/O库中的很多实现都以不可重入的方式使用全局数据结构

10.7 SIGCLD语义

SIGCLD现在貌似已经没了,了解一下就行

SIGCLD的早期处理方式:

  1. 如果进程明确地将该信号的配置设置为SIG_IGN,则调用进程的子进程将不产生僵尸进程。注意,这与默认动作(SIG_DFL)”忽略”不同。子进程终止时,将其状态丢弃。如果调用进程随后调用一个wait函数,那么它将阻塞知道所有子进程都终止,然后wait会返回-1,并将其errno设置为ECHILD。(此信号的默认配置是忽略,但这不会使上述语义起作用,必须将其配置明确指定为SIG_IGN)
  2. 如果将SIGCLD的配置设置为捕捉,则内核立即检查是否有子进程准备好被等待,如果是这样,则调用SIGCLD处理程序
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
//示例
#include "apue.h"
#include <sys/wait.h>

static void sig_cld(int);

int main() {
pid_t pid;

if (signal(SIGCLD, sig_cld) == SIGERR)
err_sys("signal error");

if ((pid = fork()) < 0) {
perror("fork error");
} else if (pid == 0) {
sleep(2);
_exit(0);
}

pause();
exit(0);
}

static void sig_cld(int signo) {
pid_t pid;
int status;

printf("SIGCLD received\n");

if (signal(SIGCLD, sig_cld) == SIGERR)
perror("signal error");

if ((pid = wait(&status)) < 0)
perror("wait error");

printf("pid = %d\n", pid);
}

这里的问题就在于signal会一直重复,因为它的语义是,每次signal的时候,如果有子进程准备wait,就再度执行信号处理函数,这样就会一直调用下去

这小节的意思就是,注意你的SIGCHLD到底是哪种语义,可能不同平台有区别,不过大部分都是SIGCHLD的常规语义

10.8 可靠信号术语和语义

这里就提到现在的信号机制了,当一个信号产生时,内核通常在进程表中以某种形式设置了一个标志。当信号采取了这种动作时,我们说向进程递送了一个信号,在信号产生和递送之间的时间间隔内(虽然以人的角度来说,很短的一段时间,但它依然存在),称信号是为决的。

进程可以选用”阻塞信号递送”。如果进程产生了一个阻塞的信号,而且对该信号的动作是捕捉或默认,则为该进程的将此信号保持为未决状态,直到解除阻塞。

如果在当前信号解除阻塞之前,产生了多次信号,只递送一次

10.9 函数kill和raise

kill函数将信号发送给进程或进程组,raise发送给自身

1
2
3
4
#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
//成功返回0,出错返回-1

kill的pid参数如下:

  1. pid > 0 ,将该信号发送给进程ID为pid的进程
  2. pid == 0 ,将信号发送给与发送进程属于同一进程组的所有进程,而且发送进程具有权限向这些进程发送信号
  3. pid < 0 ,将信号发送给进程组ID等于pid绝对值,而且发送进程具有权限向其发送信号的所有进程
  4. pid == -1 ,将该信号发送给发送进程具有权限向它们发送信号的所有进程

前面说过信号0,不被信号占用,POSIX.1将信号0定义为空信号,可以用来测定进程是否存在,将signo的参数设置为0,如果向一个不存在的进程发送信号,kill返回-1,errno设置为ESRCH

注意测试进程是否存在,不是原子操作,可能在返回测试结果的时候,进程结束了,这样测试就没有意义了

10.10 函数alarm和pause

函数alarm设置一个定时器,定时器超时时,产生SIGALRM信号。默认动作时终止调用该alarm的进程

1
2
3
#include <unistd.h>
unsigned int alram(unsigned int seconds);
//返回值0,或者以前设置闹钟时间的剩余秒数

函数pause使调用进程挂起直至捕捉到一个信号

1
2
#include <unistd.h>
int pause(void); //返回-1,errno设置为EINTR
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//示例
#include <signal.h>
#include <unistd.h>

static void sig_alrm(int signo) {
/* nothing to do, just return to wake up the pause */
}

unsigned int sleep1(unsigned int seconds) {
if (signal(SIGALRM, sig_alrm) == SIGERR)
return(seconds);
alarm(seconds);
pause();
return (alarm(0));
}

该实现的3个问题:

  1. 如果调用sleep之前,调用者已经设置了闹钟,那么sleep1的第一次alarm会将其清除,可用下列方法更正这一点:检查第一次调用alarm的返回值,若其值小于本次调用alarm的参数值,等待已有闹钟超时就行了,如果大于本次调用的值,在sleep1返回之前,重置此闹钟
  2. 程序中修改了对SIGALRM信号的配置,需要在sleep1返回之前,改回原有配置
  3. 本程序存在竞争问题,如果pause之前,信号超时,产生,然后已经被处理了,那么pause永远也无法再次收到信号,导致程序永久阻塞
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//借助setjmmp和longjmp解决的一种示例
#include <setjmp.h>
#inlcude <signal.h>
#include <unistd.h>

static jmp_buf env_alrm; /* 这个变量存储set时的栈帧状态 */

static void sig_alrm(int signo) {
longjmp(env_alrm, 1);
}

unsigned int sleep2(unsigned int seconds) {
if (signal(SIGALRM, sig_alrm) == SIGERR)
return (seconds);

//如果信号触发了,返回值变为1,就算没pause也会返回
if (setjmp(env_alrm) == 0) {
alarm(seconds);
pause();
}
return (alram(0));
}

仔细考虑,该方法仍存在问题,如果涉及到两个信号的话,比如第一个信号被触发后去执行它的函数,此时超时了,出发SIGALRM信号,提早结束了第一个信号的处理程序

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
//示例
#include "apue.h"

unsigned int sleep2(unsigned int);
static void sig_int(int);

int main() {
unsigned int unslept;

if (signal(SIGINT, sig_int) == SIGERR)
err_sys("signal(SIGINT) error");

unslept = sleep2(5);
printf("sleep2 returned: %u\n", unslept);
exit(0);
}

static void sig_int(int signo) {
int i, j;
volatile int k; /* 阻止优化编译程序 */

//这个循环保证这个程序运行时间大于sleep2的5s
printf("\nsig_int starting\n");
for (i = 0; i < 300000; i++) {
for (j = 0; j < 4000; j++) {
k += i * j;
}
}
printf("sig_int finished\n");
}

//运行结果
$ ./a.out
^c
sig_int starting
sleep2 return: 0

简单讲一下程序执行过程,捕捉SIG_INT,然后调用sleep2捕捉signal,并阻塞在此,键入ctrl+c产生SIGINT信号,执行sig_int函数,alarm超时,产生SIGALRM信号,处理sig_alrm函数,然后直接返回到sleep2,使SIGINT处理程序中断

alram的另一个作用,对可能阻塞的操作设置时间上限值。例如程序中有一个读低速设备的可能阻塞操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "apue.h"

static void sig_alrm(int);

int main() {
int n;
char line[MAXLINE];

if (signal(SIGALRM, sig_alrm) == SIGERR)
err_sys("signal(SIGALRM) error");

alarm(10);
if ((n = read(STDIN_FILENO, line, MAXLINE)) < 0)
err_sys("read error");
alarm(0);

write(STDOUT_FILENO, line, n);
exit(0);
}

static void sig_alrm(int signo) {
/* nothing to du, just return to interrupt the read */
}

两个问题:

  1. 仍然存在竞争条件,但我们知道竞争条件,算是一种概率性问题,一般这种需求的程序,alarm设置的很长,所以竞争导致的问题不会发生
  2. 如果系统调用是自动重启动的,则当SIGALRM信号处理程序返回时,read并不被中断。

10.11 信号集

信号集,用来表示多个信号的数据类型,以每一位代表一个信号,感觉linux里这种做法很多,这就是位图吧貌似

1
2
3
4
5
6
7
8
#include <signal.h>
int sigemptyset(sigset_t* set);
int sigfillset(sigset_t* set);
int sigaddset(sigset_t* set, int signo);
int sigdelset(sigset_t* set, int signo);
//4个函数成功返回0,出错返回-1
int sigismemeber(const sigset_t* set, int signo);
//若真,返回1;若假,返回0

函数sigemptyset初始化信号集,初始化(清除)由set指向的信号集,sigfillset与之相反,使每一位信号被包括,sigaddset添加一个信号,sigdelset删除一个信号

sigemptyset和sigfillset可以实现为宏:

1
2
3
#define sigemptyset(ptr) (*(ptr) = 0)
#define sigfillset(ptr) (*(ptr) = !(sigset_t)0, 0)
//借助逗号表达式,实现返回值
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
//如下为另外三个函数的非宏实现
#include <signal.h>
#include <errno.h>

/*
* <signal.h> usually defines NSIG TO include signal number 0.
*/

#define SIGBAD(signo) ((signo) <= || (signo) >= NSIG)

int sigaddset(sigset_t* set, int signo) {
if (SIGBAD(signo)) {
errno = EINVAL;
return (-1);
}
*set |= 1 << (signo - 1); /* turn bit on */
return (0);
}

int sigaddset(sigset_t* set, int signo) {
if (SIGBAD(signo)) {
errno = EINVAL;
return -1;
}
*set &= ~(1 << (signo-1)); /* turn bit off */
return (0);
}

int sigismember(const sigset_t* set, int signo) {
if (SIGBAD(signo)) {
errno = EINVAL;
return (-1);
}
return ((*set & (1 << (signo-1))) != 0);
}

10.12 函数sigprocmask

调用sigprocmask可以检测或更改,或同时进行检测和更改进程的信号屏蔽字

1
2
3
#include <signal.h>
int sigprocmask(int how, const sigset_t* restrict set, sigset_t* restrict oset);
//成功返回0,出错返回-1

set是传入想要修改的信号屏蔽字,oset是返回之前的信号屏蔽字,how可选的参数如下:

  1. SIG_BLOCK 当前信号屏蔽字和set指定信号集的并集,简单来说,就是添加set中的信号
  2. SIG_UNBLOCK 当前信号屏蔽字和set指定信号集补集的交集,简单来说,就是删除set中的信号
  3. SIG_SETMASK 直接以set来代替当前信号屏蔽字

如果set传入NULL,不论how取何值都不影响信号屏蔽字

“注意”:在调用sigprocmask后如果有任何未决的、不再阻塞的信号,则在sigprocmask返回前,至少将其中之一递交给该进程

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
//打印调用进程信号屏蔽字中的信号名示例
#include "apue.h"
#include <errno.h>

void pr_mask(const char* str) {
sigset_t sigset;
int errno_save;

errno_save = errno; /* we can be called by signal handlers */
if (sigprocmask(0, NULL, &sigset) < 0) {
err_set("sigprocmask error");
} else {
printf("%s", str);
if (sigismember(&sigset, SIGINT))
printf(" SIGINT");
if (sigismember(&sigset, SIGQUIT))
printf(" SIGQUIT");
if (sigismember(&sigset, SIGUSR1))
printf(" SIGUSR1");
if (sigismember(&sigset, SIGALRM))
printf(" SIGALRM");

/* remaining signals can go here */

printf("\n");
}

errno = errno_save;
}

10.13 函数sigpending

返回未决信号集

1
2
3
#include <signal.h>
int sigpending(sigset_t* set);
//成功返回0,出错返回-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
//示例
#include "apue.h"

static void sig_quit(int);

int main() {
sigset_t newmask, oldmask, pendmask;

if (signal(SIGQUIT, sig_quit) == SIGERR)
err_sys("can't catch SIGQUIT");

/* Block SIGQUIT and save current signal mask */

sigemptyset(&newmask);
sigaddset(&newmask, SIGQUIT);
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
err_sys("SIG_BLOCK error");

sleep(5);

if (sigpending(&pendmask) < 0)
err_sys("sigpending error");
if (sigismember(&pendmask, SIGQUIT))
printf("\nSIGQUIT pending\n");

/* Restore signal mask which unblocks SIGQUIT */

if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
err_sys("SIG_SETMASK error");
printf("SIGQUIT unblocked\n");

sleep(5);
exit(0);
}

static void sig_quit(int signo) {
printf("caught SIGQUIT\n");
if (signal(SIGQUIT, SIG_DFL) == SIG_ERR)
err_sys("can't reset SIGQUIT");
}

//执行结果
$ ./a.out
^\ 产生一次信号
SIGQUIT pending 从sleep中返回
caught SIGQUIT 在信号处理函数中
SIGQUIT unblocked 从sigprocmask返回后
^\Quit(coredump) 再次产生信号
$./a.out4
^\^\^\^\^\^\^\^\^\^\ 产生十次信号
SIGQUIT pending
caught SIGQUIT 只产生一次信号
SIGQUIT unblocked
^\Quit(coredump) 在产生信号

注意sigprocmask返回之后的printf,比信号处理中的后执行,参考上一节的 注意

10.14 函数sigaction

取代signal,我之前写的时候,signal用不了,而且这个函数能完全取代signal,除了便捷性上,signal用起来比较简单

1
2
3
4
5
6
7
8
9
10
#include <signal.h>
int sigaction(int signo, const struct sigaction* restrict act, struct sigaction* restrict oact);
//成功返回0,出错返回-1
struct sigaction {
void (*sa_handler)(int); /* 信号捕捉函数 */
sigset_t sa_mask; /* 捕捉期间添加的信号屏蔽字 */
int sa_flags; /* 一些可选参数 ,通常为0,默认属性*/
/* sa_handler 的替代,可以传递一些信息,这两个是一个Union,只能选一个 */
void (*sa_sigaction)(int, siginfo_t*, void*);
};

通常用sa_handler,当sa_flags设置为SA_SIGINFO标志时,使用sa_sigaction,sa_flags的可选参数见P279图

siginfo结构包含了信号产生原因的有关消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct siginfo {
int si_signo;
int si_errno;
int si_code;
pid_t si_pid;
uid_t si_uid;
void* si_addr;
int si_status;
un;ion sigval si_value;
}

union sigval {
int sival_int;
void* sival_ptr;
};

sa_sigaction不多说,用的比较少,用到的时候再会过来细看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//用sigaction实现signal的示例
#include "apue.h"

Sigfunc* signal(int signo, Sigfunc* func) {
struct sigaction act, oact;

act.sa_handler = func;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;

if (signo == SIGARRM) {
#ifdef SA_INTERRUPT
act.sa_flags |= SA_INTERRUPT;
#endif
} else {
act.sa_flags |= SA_RESTRAT;
}

if (sigaction(signo, &act, &oact) < 0)
return (SIG_ERR);
return (oact.sa_handler);
}

10.15 函数sigsetjmp和siglongjmp

setjmp和longjmp的改进,因为这两个函数对于是否恢复信号屏蔽字,不确定

1
2
3
4
#include <setjmp.h>
int sigsetjmp(sigjmp_buf env, int savemask);
//返回值:若直接调用,返回0;若从siglongjmp,返回非0
void siglongjmp(sigjmp_buf env, int val);

唯一区别多了savemask参数,如果saemask非0,则sigsetjmp在env中保存进程当前的信号屏蔽字,如果保存了则恢复

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
//示例
#include "apue.h"
#include <setjmp.h>
#include <time.h>

static void sig_usr1(int);
static void sig_alrm(int);
static sigjmp_buf jmpbuf;
static volatile sig_atomic_t canjmp;

int main() {
if (signal(SIGUSR1, sig_usr1) == SIG_ERR)
err_sys("signal(SIGUSR1) error");
if (signal(SIGALRM, sig_alrm) == SIG_ERR)
err_sys("signal(SIGALRM) error");

pr_mask("starting main:"); /* 10.12 pr_mask 函数 */

if (sigsetjmp(jmpbuf, 1)) {
pr_mask("ending main: ");

exit(0);
}
canjmp = 1; /* now sigsetjmp() is ok */
for (;;)
pause();
}

static void sig_usr1(int signo) {
time_t starttime;

if (canjump == 0)
return; /* unexpected signal, ignore */

pr_mask("starting sig_usr1: ");

alarm(3); /* SIGALRM in 3 seconds */
starttime = time(NULL);
for (;;) /* busy wait fo 5 seconds */
if (time(NULL) > starttime + 5)
break;
pr_mask("finishing sig_usr1: ");

canjump = 0;
siglongjmp(jmpbuf, 1);
}

static void sig_alrm(int signo) {
pr_mask("int sig_alrm: ");
}

//执行结果
$ ./a.out
starting main:
[1] 531
$ kill -USR1 531
starting sig_usr1: SIGUSR1
$ in sig_alrm: SIGUSR1 SIGALRM
finishing sig_usr1: SIGUSR1
ending main:
[1] + Done ./a.out &

如果用普通的setjump和longjump则不会恢复信号屏蔽字

image-20221027195129744

10.16 函数sigsuspend

之前视频部分分析过这个情况参考(视频部分时序竞态),sigsuspend作为一个原子操作,先恢复信号屏蔽字,然后执行pause的功能,使进程休眠。

1
2
3
#include <signal.h>
int sigsuspend(const sigset_t* sigmask);
//返回值:-1,并将errno设置为EINTR
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
//示例
#include "apue.h"

static void sig_int(int);

int main() {
sigset_t newmask, oldmask, waitmask;

pr_mask("program start: "); /* 10.12 函数 */

if (signal(SIGINT, sig_int) == SIG_ERR)
err_sys("signal(SIGINT) error");

//waitmask作为suspend解除屏蔽字的时候的临时信号屏蔽字,sigsuspend结束时,回到调用之前的屏蔽字
sigemptyset(&waitmask);
sigaddset(&waitmask, SIGUSR1);
//newmask作为当前进程的信号屏蔽字
sigemptyset(&newmask);
sigaddset(&newmask, SIGINT);

if (isgprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
err_sys("SIG_BLOCK error");

pr_mask("in critical region: ");

//以waitmask作为目前临时的信号屏蔽字
if (sigsuspend(&waitmask) != -1)
err_sys("sigsuspend error");
pr_mask("after return from sigsuspend: ");

//恢复最初的屏蔽字
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
err_sys("SIG_SETMASK error");

pr_mask("program exit: ");

exit(0);
}

static void sig_int(int signo) {
pr_mask("\nin sig_int: ");
}

//运行结果
$ ./a.out
program start:
int critical region: SIGINT
^C
in sig_int: SIGINT SIGUSR1
after return from sigsuspend: SIGINT
program exit:
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
//sigsuspend的另一个应用,等待一个信号处理程序设置一个全局变量。
#include "apue.h"
volatile sig_atomic_t quitflag;

static void sig_int(int signo) {
if (signo == SIGINT)
printf("\ninterrupt\n");
else if (signo == SIGQUIT)
quitflag = 1;
}

int main() {
sigset_t newmask, oldmask, zeromask;

if (signal(SIGINT, sig_int) == SIG_ERR)
err_sys("error");
if (signal(SIGQUIT, sig_int) == SIG_ERR)
err_sys("error");

sigemptyset(&zeromask);
sigemptyset(&newmask);
sigaddset(&newmask, SIGQUIT);

if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
err_sys("SIG_BLOCK error");

while (quitflag == 0)
sigsuspend(&zeromask);

quitflag = 0;

if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
err_sys("error");

exit(0);
}

//运行结果
$ ./a.out
^c
interrupt
^c
interrupt
^c
interrupt
^\$ //退出符退出

有一说一没看出,这里和suspend的原子操作特性有啥关系,甚至感觉不用设置屏蔽字,你就while pause,直接就能处理了,我唯一想到的就是,防止sig_int执行SIGQUIT处理的时候,被抢走调度,去执行SIGINT的,但这样也没有影响啊,执行完它的再回来调用,一样的,顶多效率会低一点,suspend能保证每一次调用SIGQUIT的时候都成功执行

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
//可以用信号实现父子进程之间的同步,这是信号应用的另一个示例
#include "apue.h"

static volatile sig_atomic_t sigflag;
static sigset_t newmask, oldmask, zeromask;

static void sig_usr(int signo) {
sigflag = 1;
}

void TELL_WAIT() {
if (signal(SIGUSR1, sig_usr) == SIG_ERR)
err_sys("signal SIGUSR1 error");
if (signal(SIGUSR2, sig_usr) == SIG_ERR)
err_sys("signal SIGUSR2 error");

sigemptyset(&zeromask);
sigemptyset(&newmask);
sigaddset(&newmask, SIGUSR1);
sigaddset(&newmask, SIGUSR2);

if (sigprocmask(SIG_BLOCK, &newmaks, &oldmask) < 0)
err_sys("SIG_BLOCK error");
}

void TELL_PARENT(pid_t pid) {
kill(pid, SIGUSR2);
}

void WAIT_PARENT() {
while (sigflag == 0)
sigsuspend(&zeromask);
sigflag = 0;

if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
err_sys("SIG_SETMASK error");
}

void TELL_CHILD(pid_t pid) {
kill(pid, SIGUSR1);
}

void WAIT_CHILD() {
while (sigflag == 0)
sigsuspend(&zeromask);
sigflag = 0;

if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
err_sys("SIG_SETMASK error");
}

SIGUSR1由父进程发给子进程,SIGUSR2由子进程发给父进程

总之sigsuspend只是解除屏蔽和pause的原子操作,其他慢速系统调用如read如果有类似情况,则无效,不能用suspend解决

10.17 函数abort

使程序异常终止

1
2
#include <stdlib.h>
void abort();
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
//abort的一种实现,有一说一没看懂,为什么要这样,大概看了下,首先防止了调用者在调用abort之前对SIGABRT信号的一些配置,确保一定能调用成功,但为什么要fflush和,调用了两次kill(SIGARBT),就没太明白,第二次调用是确保一定成功执行吗
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void abort() {
sigset_t mask;
struct sigaction action;

/* Caller can't ignore SIGABRT, if so reset to default */
sigaction(SIGABRT, NULL, &action);
if (action.sa_handler == SIG_IGN) {
action.sa_handler = SIG_DFL;
sigaction(SIGABRT, &action, NULL);
}
if (action.sa_handler == SIG_DFL)
fflush(NULL); /* flush all open stdio streams */

/* Caller can't block SIGABRT; make sure it't unblocked */
sigfillset(&mask);
sigdelset(&mask, SIGABRT); /* mask has only SIGABRT turned off */

sigprocmask(SIG_SETMASK, &mask, NULL);
kill(getpid(), SIGABRE); /* send the signal */

/* if we're here, process caught SIGABRT and returned */
fflush(NULL);
action.sa_handler = SIG_DFL;
sigaction(SIGABRT, &action, NULL); /* reset to default */
sigprocmask(SIG_ABRT, &mask, NULL); /* just in case ... */
kill(getpid(), SIGABRT); /* and one more time */
exit(1); /* this should be excuted ... */
}

10.18 函数system

POSIX.1要求system忽略SIGINT和SIGQUIT,阻塞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
35
36
37
38
39
40
41
42
43
44
45
46
47
//8.13的system实现,不完整,因为他没有考虑上述需求,如果拿它运行ed编辑器
$ ./a.out
a 将正文追加至编辑器
Here is one line of text
. 行首的点停止追加方式
1,$p 打印缓冲区中的第一行至最后一行,以便观察内容
Here is one line of text
w temp.foo 将缓冲区写至一文件
25 编辑器写了25字节
q 离开编辑器
caught SIGCHLD

//如果键入中断符
#include <apue.h>
static void sig_int(int signo) {
printf("caught SIGINT\n");
}

static void sig_chld(int signo) {
printf("caught SIGCHLD\n");
}

int main() {
if (signal(SIGINT, sig_int) == SIG_ERR)
err_sys("signal SIGINT error");
if (signal(SIGCHLD, sig_chld) == SIG_ERR)
err_sys("signal SIGCHLD error");
if (system("/bin/ed") < 0)
err_sys("system error");

exit(0);
}

//结果
$ ./a.out
a 将正文追加至编辑器
hello, world
. 行首的点停止追加方式
1,$p 打印缓冲区中的第一行至最后一行,以便观察内容
hello, world
w temp.foo 将缓冲区写至一文件
13 编辑器写了13字节
^c 键入中断符
? 编辑器捕捉到信号,打印问号
caught SIGINT 父进程执行统一操作
q 离开编辑器
caught SIGCHLD

这里的意思是,如果不做处理的话,键入中断符,会使中断信号传送给前台进程组中所有的进程。在这一例中,SIGINT被送给3个前台进程(shell进程忽略此信号),但是当system运行另一个程序的时候,不应使父子进程两周都捕捉终端产生的两个信号:中断和退出。因为由system执行的命令可能是交互式命令

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
//system的另一个实现
#include <sys/wait.h>
#include <errno.h>
#inclued <signal.h>
#include <unistd.h>

int system(const char* cmdstring) { /* with appropriate signal handling */
pid_t pid;
int status;
struct sigaction ignore, saveintr, savequit;
sigset_t chldmask, savemask;

if (cmdstring == NULL)
return 1; /* always a command processor with UNXI */

ignore.sa_handler = SIG_IGN; /* ignore SIGINT and SIGQUIT */
sigemptyset(&ignore.sa_mask);
ignore.sa_flags = 0;

if (sigaction(SIGINT, &ignore, &saveintr) < 0)
return -1;
if (sigaction(SIGQUIT, &ignore, &savequit) < 0)
return -1;

sigemptyset(&chldmask); /* now block SIGCHLD */
sigaddset(&chldmask, SIGCHLD);
if (sigprocmask(SIG_BLOCK, &chldmask, &savemask) < 0)
return -1;

if ((pid = fork()) < 0) {
status = -1; /* probably out of processes */
} else if (pid == 0) {
/* restore previous signal acton & reset signal mask */
sigaction(SIGINT, &saveintr, NULL);
sigaction(SIGQUIT, &savequit, NULL);
sigprocmask(SIG_SETMASK, &savemask, NULL);

execl("/bin/sh", "sh", "-c", cmdstring, (char*)0);
_exit(127);
} else {
while (waitpid(pid, &status, 0) < 0)
if (errno != EINTR) {
status = -1; /* error other than EINTR from waidpid() */
break;
}
}

/* restore previous signal acton & reset signal mask */
if (sigaction(SIGINT, &saveintr, NULL) < 0)
return -1;
if (sigaction(SIGQUIT, &savequit, NULL) < 0)
return -1;
if (sigprocmask(SIG_SETMASK, &savemask, NULL) < 0)
return -1;

return status;
}

与之前实现的区别:

  1. 当我们键入中断符或退出符时,不向调用进程发送信号
  2. 当ed命令终止时,不想调用进程发送SIGCHLD信号。因为在system函数末尾调用sigprocmask之前,一直被阻塞,知道waitpid获取子进程的终止状态后

system的返回值

system的返回值,时shell的终止状态

10.19 函数sleep、nanosleep和clock_nanosleep

1
2
3
#include <unistd.h>
unsigned int sleep(unsigned int seconds);
//返回值:0或未休眠完的秒数

此函数使得进程被挂起知道满足下面两个条件之一:

  1. 已经过了seconds所指定的墙上时钟时间
  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
31
32
33
34
35
36
37
38
39
//sleep的可靠实现
#include "apue.h"

static void sig_alrm(int signo) {
/* nothing to do, just returning wakes up sigsuspend() */
}

unsigned int sleep(unsigned int seconds) {
struct sigaction newact, oldact;
sigset_t newmask, oldmask, suspmask;
unsigned int unslept;

/* set our handler, save previous information */
newact.sa_handler = sig_alrm;
sigemptyset(&newact.sa_mask);
newact.sa_flag = 00;
sigaction(SIGALRM, &newmask, &oldmask);

alrarm(seconds);
suspmask = oldmask;

/* make sure SIGALRM ins't blocked */
sigdelset(&suspmask, SIGALRM);

/* wait for any signal to be caught */
sigsuspend(&suspmask);

/* some signal has been caught, SIGALRM is no blocked */

unslept = alarm(0);

/* reset previous action */
sigaction(SIGALRM, &oldact, NULL);

/* reset signal mask, which unblocks SIGALRM */
sigprocmask(SIG_SETMASK, &oldmask, NULL);

return(unslept);
}
1
2
3
#include <time.h>
int nanosleep(const struct timespec* reqtp, struct timespec* remtp);
//若休眠到要求的时间返回0,出错返回-1

与sleep类似,但提供了纳秒级的精度

1
2
3
#include <time.h>
int clock_nanosleep(clockid_t clock_id, int flags, const struct timespec* reqtp, struct timespec* remtp);
//若休眠到要求的时间返回0,出错返回-1

总之也类似,用到再看 P300

10.20 函数sigqueue

使用信号排队必须做以下几件事:

  1. 使用sigaction函数安装信号处理程序时指定SA_SIGINFO标志,如果没有给出这个标志,信号会延迟,但信号是否进入队列取决于具体实现
  2. 在sigaction结构的sa_sigaction成员中提供信号处理程序。实现可能允许用户使用sa_handler字段,但不能获取sigqueue发送出来的额外信息
  3. 使用sigqueue函数发送信号
1
2
#include <signal.h>
int sigqueue(pid_t pid, int signo, const union sigval value);

遇到再看吧,这里讲的也很简洁,就大概提了一下

10.21 作业控制信号

POSIX.1认为以下6个信号与作业控制有关

  1. SIGCHLD 子进程已停止或终止
  2. SIGCONT 如果进程已停止,则使其继续运行
  3. SIGSTOP 停止信号(不能被捕捉或忽略)
  4. SIGTSTP 交互式停止信号
  5. SIGTTIN 后台进程组成员读控制终端
  6. SIGTTOU 后台进程组成员写控制终端

除SIGCHLD之外,大多数应用程序并不处理这些信号,交互式shell通常会处理这些信号的所有工作。

在作业控制信号间有某些交互。当对一个进程产生4种停止信号(SIGTSTP,SIGSTOP,SIGTTIN,SIGTTOU)中的任意一种时,对该进程的任一未决SIGCONT信号就被丢弃,于此类似,当对一个进程产生SIGCONT信号时,同意进程的任一未决停止信号就被丢弃

1
2
3
4
5
6
7
8
9
//示例:演示了一个程序处理作业控制时通常所用的代码序列。该程序只时将其标准输入复制到其标准输出,而在信号
//处理程序中以注释形式给出了管理屏幕的程序所执行的典型操作
#include "apue.h"

static void sig_tstp(int signo) { /* signal handler for SIGTSTP
sigset_t mask;

/* ... move cursor to lower left corner, reset tty mode ... */
}

10.22 信号名和编号

如何在信号名和编号之间进行映射,有些系统提供数组

1
extern char* sys_siglist[];

可以使用psignal函数可移植地打印与信号编号对应的字符串

1
2
#include <signal.h>
void psignal(int signo, const char* msg);

如果在sigaction中有siginfo结构,可以使用psiginfo,能访问更更多信息

1
2
#include<signal.h>
void psiginfo(const siginfo_t* info, const char* msg);

如果只需要信号的字符描述部分,也不需要把它写到标准错误中

1
2
#include <signal.h>
char* strsignal(int signo);

没啥难点,用到再细看

10.23 小结

详细的把信号的基本内容都讲了,还涉及到一些具体实现,以及早期实现的缺点

第11章 线程

11.1 引言

简单来说在单进程里执行多个任务,单进程也可以看作只有线程的进程

11.2 线程概念

线程的好处:

  1. 简化处理异步事件的代码。就是用起来更简单吧
  2. 多个进程必须使用操作系统提供的复杂机制才能实现内存和文件描述符的共享。
  3. 改善响应时间,提高程序的吞吐量

每个线程都包含表示执行环境所必需的信息,其中包括进程中的标识线程的ID,一组寄存器值,栈,调度优先级和策略,信号屏蔽字,errno变量以及线程私有数据

11.3 线程标识

线程ID用pthread_t数据类型来表示,由于实现可能是一个结构体,所以对于线程ID的比较,要用以下函数

1
2
3
#include <pthread.h>
int pthread_equal(pthread_t tid1, pthread_t tid2);
//相等返回非0;否则返回0

调用pthread_self获得自身线程ID

1
2
#include <pthread.h>
pthread_t pthread_self();

11.4 线程创建

1
2
3
#include <pthread.h>
int pthread_create(pthread_t* restrict tidp, const pthread_attr_t* restirct attr, void* (*start_rtn)(void*), void* restirct arg);
//成功返回0,出错返回错误编号

tidp保存线程ID,atrr创建线程的一些属性,start_rtn线程函数,arg向线程函数传入的参数

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
//示例
#include "apue.h"
#include <pthread.h>

pthread_t ntid;

void printids(const char* s) {
pid_t pid;
pthread_t tid;

pid = getpid();
tid = pthread_self();
printf("%s pid %lu tid %lu (0x%lx)\n", s, (unsigned long)pid, (unsigned long)tid, (unsigned long)tid);
}

void* thr_fn(void* arg) {
printids("new thread: ");
return((void*)0);
}

int main() {
int err;

err = pthread_create(&ntid, NULL, thr_fn, NULL);
if (err != 0)
err_exit(err, "can't create thread");
printids("main thread: ");
sleep(1);
return 0;
}

//执行结果
$ ./a.out
main thread: pid 37396 tid 673290208 (0x28201140)
new thread: pid 37396 tid 673280320 (0x28217140

11.5 线程终止

如果进程中的任意线程调用了exit、_Exit或者 _exit,那么整个进程就会终止。单个线程可以通过3种方式退出:

  1. 线程可以简单地从启动例程种返回,返回值是线程的退出码
  2. 线程可以被同一进程中的其他线程取消
  3. 线程调用pthread_exit
1
2
#include <pthread.h>
void pthread_exit(void* rval_ptr);

rval_ptr是一个无类型指针,用来保存返回值,进程中的其他线程可以通过调用pthread_join函数访问到这个指针

1
2
3
#include <pthread.h>
int prhread_join(pthread_t thred, void** rval_ptr);
//成功返回0,出错返回错误编号

pthread_join类似于进程控制中的waitpid,如果对返回值不关心,可以设置rval_ptr为NULL

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
//示例
#include "apue.h"
#include <pthread.h>

void* thr_fn1(void* arg) {
printf("thread 1 returning\n");
return((void*)1);
}

void* thr_fn2(void* arg) {
printf("thread 2 exiting\n");
pthread_exit((void*)2);
}

int main() {
int err;
pthread_t tid1, tid2;
void* tret;

err = pthread_create(&tid1, NULL, thr_fn1, NULL);
if (err != 0) {
err_exit(err, "can't create thread 1");
}

err = pthread_create(&tid2, NULL, thr_fn2, NULL);
if (err != 0) {
err_exit(err, "can't create thread 2");
}

err = ptrhead_join(tid1, &tret);
if (err != 0) {
err_exit(err, "can't join with thread 1");
}
printf("thread 1 exit code %ld\n", (long)tret);

err = pthread_join(tid2, &tret);
if (err != 0) {
err_exit(err, "can't join with thread 2");
}
printf("thread 2 exit code %ld\n", (long)tret);

exit(0);
}

//运行结果
$ ./a.out
thread 1 returning
thread 2 exiting
thread 1 exit code 1
thread 2 exit code 2

线程可以通过调用pthread_cancel函数来请求取消同一进程中的其他线程

1
2
3
#include <pthread.h>
int pthread_cancel(pthread_t tid);
//成功返回0,否则返回错误编号

线程可以安排它退出时需要调用的函数,这与进程在退出时可以用atexit函数安排退出是类似的。称为 线程清理处理程序 。处理程序记录在栈中,它们的执行顺序与它们注册时相反

1
2
3
#include <pthread.h>
void pthread_cleanup_push(void (*rtn)(void*), void* arg);
void pthread_cleanup_pop(int execute);

当线程执行以下动作时,清理函数rtn是由pthread_cleanup_push函数调度的,掉用时有一个arg参数:

  1. 调用pthread_exit时
  2. 响应取消请求时
  3. 用非零execute参数调用pthread_cleanup_pop时

如果execute参数设置为0,清理函数将不被调用

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
//示例
#include "apue.h"
#include <pthread.h>

void cleanup(void* arg) {
printf("cleaning: %s\n", (char*)arg);
}

void* thr_fn1(void* arg) {
printf("thread 1 start\n");

pthread_cleanup_push(cleanup, "thread 1 first handler");
pthread_cleanup_push(cleanup, "thread 1 second handler");
printf("thread 1 push complete\n");

if (arg)
return((void*)1); //返回而终止

pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
return ((void*)1);
}

void* thr_fn2(void* arg) {
printf("thread 2 start\n");

pthread_cleanup_push(cleanup, "thread 2 first handler");
pthread_cleanup_push(cleanup, "thread 2 second handler");
printf("thread 2 push complete\n");

if (arg)
pthread_exit((void*)2); //调用pthread_exit终止,满足执行处理函数的三个条件之一

pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
pthread_exit((void*)2);
}

int main() {
int err;
pthread_t tid1, tid2;
void* tret;

err = pthread_create(&tid1, NULL, thr_fn1, (void*)1);
if (err != 0)
err_exit(err, "can't create thread 1");

err = pthread_create(&tid2, NULL, thr_fn2, (void*)1);
if (err != -)
err_exit(err, "can't create thread 2");

err = pthead_join(tid1, &tret);
if (err != 0)
err_exit(err, "can't join with thread 1");
printf("thread 1 exit code %ld\n", (long)tret);

err = pthread_join(tid2, &tret);
if (err != 0)
err_exit(err, "can't join with thread 2");
printf("thread 2 exit code %ld\n", (long)tret);

exit(0);
}

线程分离,分离后,线程的底层存储资源可以在线程终止时,立即被收回

1
2
3
#include <pthread.h>
int pthread_detach(pthread_t tid);
//成功返回0,出错返回错误编号

11.6 线程同步

线程同步的需求出现在这样一个背景,多个线程对一个变量进行读写,因为写的操作需要两个存储器周期,如果在这两个存储器周期之间,另一个线程又进行操作,那么就会出现竞争导致的问题,为了让线程同步,引入了后续的概念,用于实现线程同步

image-20221031135915620

image-20221031135929186

1.互斥量

互斥量以pthread_mutex_t 数据类型来表示,使用之前需要对他进行初始化

1
2
3
4
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t* restirct mutex, const pthread_mutexattr_t* restritct attr);
int pthread_mutex_destroy(pthread_mutex_t* mutex);
//成功返回0,出错返回错误编号

对互斥量加锁,调用pthrea_mutex_lock 。如果已经上锁,调用线程将阻塞直到互斥量被解锁。

1
2
3
4
5
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_trylock(pthread_mutex_t* mutex);
int pthread_mutex_unlock(pthread_mutex_t* mutex);
//成功返回0,出错返回错误编号

trylock的区别是不会阻塞,如果加锁时,mutex已锁住,trylock失败,返回EBUSY

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
//示例 一个类似shared_ptr机制的例子,引用计数,只有到0的时候才会删除,每次更改计数的时候,对计数上锁,防止竞争

#include <stdlib.h>
#include <pthread.h>

struct foo {
int f_count;
pthread_mutex_t f_lock;
int f_id;
/* ... more stuff here ... */
};

struct foo* foo_alloc(int id) {
struct foo* fp;

if ((fp = malloc(sizeof(struct foo))) != NULL) {
fp->f_count = 1;
fp->f_id = id;
if (pthread_mutex_init(&fp->f_lock, NULL) != 0) {
free(fp);
return NULL;
}
/* ... continue initialization ... */
}
return (fp);
}

void foo_hold(struct foo* fp) {
pthread_mutex_lock(&fp->f_lock);
fp->f_count++;
pthread_mutex_unlock(&fp->f_lock);
}

void foo_rele(struct foo* fp) {
pthread_mutex_lock(&fp->f_lock);
if (--fp->count == 0) {
//感觉这里有点问题,如果解锁后另一个线程,请求加锁了,那现在解锁后那边解除阻塞
//这边后面销毁不就出问题了吗,因为引用计数已经不是0了,感觉锁不能放在foo里面,
//应该是另一个全局变量,保证在引用计数为0的时候,能删除,其他地方感觉也需要对这个进行一个判断
//被加锁的foo是否已经被销毁了,如果被销毁则重新初始化?
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_destroy(&fp->f_lock);
free(fp);
} else {
pthread_mutex_unlock(&fp->f_lock);
}
}
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
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
//示例11-10的更新

#include <stdlib.h>
#include <pthread.h>


#define NHASH 29
#define HASH(id) (((unsigned long)id)%NHASH)

struct foo* fh[NHASH];

//静态初始化
pthread_mutex_t hashlock = PTHREAD_MUTEX_INITIALIZER;

struct foo {
int f_count;
phtead_mutex_t f_lock;
int f_id;
struct foo* f_next; /* protected by hashlock */
/* ... more stuff here ... */
};

struct foo* foo_alloc(int id) {
struct foo* fp;
int idx;

if ((fp = malloc(sizeof(struct foo))) != NULL) {
fp->f_count = 1;
fp->f_id = id;
if (pthread_mutex_init(&fp->f_lock, NULL) != 0) {
free(fp);
return NULL;
}
idx = HASH(id);
pthread_mutex_lock(&hashlock);
fp->f_next = fh[idx];
fh[idx] = fp;
pthread_mutex_lock(&fp->f_lock);
pthread_mutex_unlock(&hashlock);
/* ... continue initialization ... */
pthread_mutex_unlock(&fp->f_lock);
}
return (fp);
}

void foo_hold(struct foo* fp) {
pthread_mutex_lock(&fp->f_lock);
fp->f_count++;
pthread_mutex_unlock(&fp->f_lock);
}

struct foo* foo_find(int id) {
struct foo* fp;

pthread_mutex_lock(&hashlock);
for (fp = fh[HASH(id)]; fp != NULL; fp = fp->f_next) {
if (fp->f_id == id) {
foo_hold(fp);
break;
}
}
pthread_mutex_unlock(&hashlock);
return(fp);
}

void foo_rele(struct foo* fp) {
struct foo* tfp;
int idx;

pthread_mutex_lock(&fp->f_lock);
if (fp->f_count == 1) {
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_lock(&hashlock);
pthread_mutex_lock(&fp->f_lock);

//重新检查,因为之前被阻塞的时候,可能发生了变动
if (fp->f_count != 1) {
fp->f_count--;
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_unlock(&hashlock);
return;
}

idx = HASH(fp->f_id);
tfp = fh[idx];
if (tfp == fp) {
fh[idx] = fp->f_next;
} else {
while (tfp->f_next != fp)
tfp = tfp->f_next;
tfp->f_next = fp->f_next;
}
pthread_mutex_unlock(&hashlock);
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_destroy(&fp->f_lock);
free(fp);
} else {
fp->f_count--;
pthread_mutex_unlock(&fp->f_lock);
}
}

可以看出两把锁的时候,设计麻烦复杂了很多

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
//改进
#include <stdlib.h>
#include <pthread.h>

#define NHASH 29
#define HASH(id) (((unsigned long)id)%NHASH)

struct foo* fh[NHASH];
pthread_mutex_t hashlock = PTHREAD_MUTEX_INITIALIZER;

struct foo {
int f_count;
pthread_mutex_t f_lock;
int f_id;
struct foo* f_next;
};

struct foo* foo_alloc(int id) {
struct foo* fp;
int idx;

if ((fp = malloc(sizeof(struct foo))) != NULL) {
fp->f_count = 1;
fp->f_id = id;
if (pthread_mutex_init(&fp->f_lock, NULL) != 0) {
free(fp);
return NULL;
}
idx = HASH(id);
pthread_mutex_lock(&hashlock);
fp->f_next = fh[idx];
fh[idx] = fp;
pthread_mutex_lock(&fp->f_lock);
pthread_mutex_unlock(&hashlock);
/* ... continue initialization ... */
pthread_mutex_unlock(&fp->f_lock);
}
}

void foo_hold(struct foo* fp) {
pthread_mutex_lock(&hashlock);
fp->f_count++;
pthread_mutex_unlock(&hashlock);
}

struct foo* foo_find(int id) {
struct foo* fp;

pthread_mutex_lock(&hashlock);
for (fp = fh[HASH(id)]; fp != NULL; fp = fp->f_next) {
if (fp->f_id == id) {
foo_hold(fp);
break;
}
}
pthread_mutex_unlock(&hashlock);
return(fp);
}

void foo_rele(struct foo* fp) {
struct foo* tfp;
int idx;

pthread_mutex_lock(&hashlockk);
if (--fp->f_count == 0) {
idx = HASH(fp->f_id);
tfp = fh[idx];
if (tfp == fp) {
fh[idx] = fp->f_next;
} else {
while (tfp->f_next != fp)
tfp = tfp->f_next;
tfp->f_next = fp->f_next;
}
pthread_mutex_unlock(&hashlock);
pthread_mutex_destroy(&fp->f_lock);
free(fp);
} else {
fp->f_count--;
pthread_mutex_unlock(&hashlock);
}
}

如果锁少了,粒度太粗,就会出现很多线程阻塞等待相同的锁,这可能并不会改善并发现。如果锁多了,粒度细,过多锁的开销会使系统性能受影响,其代码更复杂。需要在代码复杂性和性能之间找到平衡

3.函数pthread_mutex_timedlock

与pthread_mutex_lock基本类似,但设置了阻塞时间,再超过时间值后,不再进行加锁,而是返回错误码ETIMEDOUT

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
#include <pthread.h>
#include <time.h>
int pthread_mutex_timedlock(pthread_mutex_t* restrict mutex, const struct timespec* restrict tsptr);
//成功返回0,出错返回错误码
//示例
#include "apue.h"
#include <pthread.h>

int main() {
int err;
struct timespec tout;
struct tm* temp;
char buf[64];
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

//第一次加锁
pthread_mutex_lock(&lock);
printf("mutex is locked\n");

clock_gettime(CLOCK_REALTIME, &tout);
tmp = localtime(&tout.tv_sec);
strftime(buf, sizeof(buf), "%r", tmp);
printf("current time is %s\n", buf);
tout.tv_sec += 10;

//第二次加锁(重复加锁)
err = pthread_mutex_timedlock(&lock, &tout);
clock_gettime(CLOCK_REALTIME, &tout);
tmp = localtime(&tout.tv_sec);
strftime(buf, sizeof(buf), "%r", tmp);
printf("the time is now %s\n", buf);

if (err == 0)
printf("mutex locked again!\n");
else
printf("can't lock mvtex again:%s\n", strerror(err));
exit(0);
}

不要在实际中这样写,可能导致死锁

4.读写锁

三种状态,读锁,写锁,未上锁,写锁会阻塞其他操作,读锁的时候,其他的读锁操作不会被阻塞,但如果有一个写锁操作随后,写锁操作之后的其他操作会被阻塞,防止读锁长期占用读写锁

1
2
3
4
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t* restrict rwlock, const pthread_rwlockattr_t* restrict attr);
int phtread_rwlock_destroy(pthread_rwlock_t* rwlock);
//成功返回0,出错返回错误编号

同样用PTHREAD_RWLOCK_INITIALIZER常量可以执行静态初始化

1
2
3
4
5
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t* rwlock);
//成功返回0,出错返回错误编号

部分实习可能对读锁共享模式下的读写锁次数进行限制,注意这方面的错误

1
2
3
4
5
//同样的try版本
#include <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t* rwlock);
//成功返回0,出错返回错误编号
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
//示例,读写锁的使用,作业请求队列由单个读写锁保护 图11-1的一种可能实现
#include "apue.h"
#include <pthread.h>

struct job {
struct job* j_next;
struct job* j_prev;
pthread_t j_id; /* tell which thread handles this job */
/* ... more stuff here ... */
};

struct queue {
struct job* q_head;
struct job* q_tail;
pthread_rwlock_t q_lock;
};

/* Initialize a queue */
int queue_init(struct queue* qp) {
int err;

qp->q_head = NULL;
qp->q_tail = NULL;
err = pthread_rwlock_init(&qp->q_lock, NULL);
if (err != 0)
return(err);

/* continue initialization */

return 0;
}

/* Insert a job at the head of queue */
void job_insert(struct queue* qp, struct job* jp) {
pthread_rwlock_wrlock(&qp->q_lock);
jp->j_next = qp->q_head;
jp->j_prev = NULL;
if (qp->q_head != NULL)
qp->q_head->j_prev = jp;
else
qp->q_tail = jp;
qp->q_head = jp;
pthread_rwlock_unlock(&qp->q_lock);
}

/* append a job on the tail of the queue */
void job_append(struct queue* qp, struct job* jp) {
pthread_rwlock_wrlock(&qp->q_lock);
jp->j_next = NULL;
jp->j_prev = qp->q_tail;
if (qp->q_tail != NULL)
qp->q_tail->j_next = jp;
else
qp->q_head = jp;
qp->q_tail = jp;
pthread_rwlock_unlock(&qp->q_lock);
}

/* remove the given job from a queue */
void job_remove(struct queue* qp, struct job* jp) {
pthread_rwlock_wrlock(&qp->q_lock);
if (jp == qp->q_head) {
qp->q_head = jp->j_next;
if (qp->q_tail == jp)
qp->q_tail = NULL;
else
jp->j_next->j_prev = jp->j_prev;
} else if (jp == qp->q_tail) {
qp->q_tail = jp->j_prev;
jp->j_prev->j_next = jp->j_next;
} else {
jp->j_prev->j_next = jp->j_next;
jp->j_next->j_prev = jp->j_prev;
}
pthread_rwlock_unlock(&qp->q_lock);
}

/* find a job for the given thread ID */
struct job* job_find(struct queue* qp, pthread_t id) {
struct job* jp;

if (pthread_rwlock_rdlock(&qp->lock, NULL))
return (NULL);

for (jp = qp->q_head; jp != NULL; jp = jp->j_next)
if (pthread_equal(jp->j_id, id))
break;

pthread_rwlock_unlock(&qp->lock);
return (jp);
}
5.带有超时的读写锁
1
2
3
4
5
#include <pthread.h>
#include <time.h>
int pthread_rwlock_timedrdlock(pthread_rwlock_t* restrict rwlock, const struct timespec* restrict tsptr);
int pthread_rwlock_timedwrlock(pthread_rwlock_t* restrict rwlock, const struct timespec* restrict tsptr);
//成功返回0,否则返回错误编号
6.条件变量

条件变量给多个线程提供了一个会合的场所。条件变量与互斥量一起使用时,允许线程已无竞争的方式等待特定的条件发生

1
2
3
4
#include <pthread.h>
int pthread_cond_init(pthread_cond_t* restrict cond, const pthread_condattr_t* restrict attr);
int pthread_cond_destroy(pthread_cond_t* cond);
//成功返回0,否则返回错误编号

同样也可以用常量PTHREAD_COND_INITIALIZER执行静态初始化

1
2
3
4
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t* restrict cond, pthread_mutex_t* restrict mutex);
int pthread_cond_timedwait(pthread_cond_t* restrict cond, pthread_mutex_t* restrict mutex, const struct timespec* restrict tsptr);
//成功返回0,出错返回错误编号
1
2
3
4
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t* cond);
int pthread_cond_broadcast(pthread_cond_t* cond);
//成功返回0,出错返回错误编号

注意在改变条件状态以后,再给线程发信号

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
//示例
#include "apue.h"

struct msg {
struct msg* m_next;
/* more stuff here */
};

struct msg* workq;

pthread_cond_t qready = PTHREAD_COND_INITIALIZER;
pthreda_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;

void process_msg() {
struct msg* mp;

for (;;) {
pthread_mutex_lock(&qlock);

while (workq == NULL)
pthread_cond_wait(&qready, &qlock);

mp = workq;
workq = mp->m_next;
pthread_mutex_unlock(&q_lock);
/* now process the message mp */
}
}

void enqueue_msg(struct msg* mp) {
pthread_mutex_lock(&qlock);
mp->m_next = workq;
workq = mp;
pthread_mutex_unlock(&qlock);
pthread_cond_signal(&qready);
}

条件是工作队列的状态,互斥量保护条件。在while循环中判断条件,

7. 自旋锁

与上述锁通过休眠实现阻塞的情况不同,自旋锁在获取锁之前一直处于忙等(自旋)阻塞状态。自旋锁可以用于以下情况: 锁被持有的时间短,而且线程并不希望在重新调度上花费太多成本。

1
2
3
4
#included <pthread.h>
int pthread_spin_init(pthread_spinlock_t* lock, int pshaed);
int pthread_spin_destroy(pthread_spinlock_t* lock);
//成功返回0,否则返回错误编号

pshared参数表示进程的共享属性,如果它设为PTHREAD_PROCESS_SHARED,则自旋锁能被可访问锁底层内存的线程所获取,即便那些线程属于不同进程,否则参数设为PTHREAD_PROCESS_PRIVATE,自旋锁只能被初始化该锁的进程内部的线程所访问

1
2
3
4
5
#include <pthread.h>
int pthread_spin_lock(pthread_spinlock_t* lock);
int pthread_spin_trylock(pthread_spinlock_t* lock);
int pthread_spin_unlock(pthread_spinlock_t* lock);
//成功返回0,否则返回错误编号

重复加锁和对未加锁的自旋锁解锁都是未定义行为

8.屏障

屏障是用户协调多个线程并行工作的同步机制。pthread_join函数就是一种屏障,允许一个线程等待,直到另一个线程退出。但是屏障对象的概念更广,它们允许任意数量的线程等待,直到所有线程完成处理工作,而线程不需要退出。所有线程到达屏障后,可以接着工作。

1
2
3
4
#include <pthread.h>
int pthread_barrier_init(pthread_barrier_t* restrict barrier, const pthread_barrierattr_t* restrict attr, unsigned int count);
int phtread_barrier_destroy(pthread_barrier_t* barrier);
//成功返回0,否则返回错误编号

count参数指定,在允许所有线程继续允许之前,必须到达屏障的线程数目。可以使用pthread_barrier_wait函数来表明,线程已完成工作,准备等待所有其他线程赶上来

1
2
3
#include <pthread.h>
int pthread_barrier_wait(pthread_barrier_t* barrier);
////成功返回0或者PTHREAD_BARRIER_SERIAL_THREAD,否则返回错误编号

对于任意一个线程,pthread_barrier_wait函数返回了PTHREAD_BARRIER_SERIAL_THREAD。剩下线程看到的返回值是0,这使得一个线程可以作为主线程

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
//示例
#include "apue.h"
#include <pthread.h>
#include <limits.h>
#include <sys/time.h>

#define NTHR 8 /* number of threads */
#define NUMNUM 8000000L /* number of numbers to sort */
#define THUM (NUMNUM/NTHR) /* number of sort per thread */

long nums[NUMNUM];
long snums[NUMNUM];

pthread_barrier_t b;

#ifdef SOLARIS
#define heapsort qsort
#else
extern int heapsort(void*, size_t, size_t, int (*)(const void*, const void*));
#endif

/* compare two long integers (helper function for heapsort) */
int complong(const void* arg1, const void* arg2) {
long l1 = *(long*)arg1;
long l2 = *(long*)arg2;

if (l1 == l2)
return 0;
else if (l1 < l2)
return -1;
else
return 1;
}

/* Worker thread to sort a portion of the set of numbers */
void* thr_fn(void* arg) {
long idx = (long)arg;

heapsort(&nums[idx], THUM, sizeof(long), complong);
pthread_barrier_wait(&b);

/* Go off and perform more work ... */
return ((void*)0);
}

/* Merge the results of the individual sorted ranges */
void merge() {
long idx[NTHR];
long i, minidx, sidx, num;

for (i = 0; i < NTHR; i++)
idx[i] = i*TNUM;
for (sidx = 0; sidx < NUMNUM; sidx++) {
num = LONG_MAX;
for (i = 0; i < NTHR; i++) {
if ((idx[i] < (i+1) * TNUM) && (nums[idx[i]] < num)) {
num = nums[idx[i]];
minidx = i;
}
}
snums[sidx] = nums[idx[minidx]];
idx[minidx]++;
}
}

int main() {
unsigned long i;
struct timeval start, end;
long long startusec, endusec;
double elapsed;
int err;
pthread_t tid;

/* create the initial set of number to sort */
srandom(1);
for (i = 0; i< NUMNUM; i++)
num[i] = random();

/* create 8 thread to sort the numbers */
gettimeofday(&start, NULL);
pthread_barrier_init(&b, NULL, NTHR+1);
for (i = 0; i< NTHR; i++) {
err = pthread_create(&tid, NULL, thr_fn, (void*)(i*THUM));
if (err != 0)
err_exit(err, "can't create thread");
}

pthread_barrier_wait(&b);
merge();
gettimeofday(&end, NULL);

/* print the sorted list */

startusec = start.tv_sec * 1000000 + start.tv_usec;
endusec= end.tv_sec * 1000000 + end.tv_usec;
elapsed = (double)(endusec - startusec) / 1000000.0;

printf("sort took %.4f seconds\n", elapsed);
for (i = 0; i < NUMNUM; i++)
printf("%ld\n", snums[i]);
exit(0);
}

一个多线程排序的例子,可以看出barrier是怎么使用的,等待所有线程都完成各自的排序工作后,再执行merge将结果合并

11.7 小结

前面的大部分之前视频部分已经看过了,自旋锁没太看懂使用场合,屏障看起来是个不错的同步机制,更符合我对多线程的理解了,之前视频部分没讲这个函数,导致觉得多线程同步起来只能通过锁的话,写起来感觉太复杂了

第12章 线程控制

12.1 引言

本章讲继续前一章,一些更详细的内容。

12.2 线程限制

线程的一些限制,可以通过sysconf函数(2.5.4节)进行查询

限制名称 描述 name参数
PTHREAD_DESTRUCTOR_ITERATIONS 线程退出时操作系统实现试图销毁线程特定数据的最大次数(12.6) _SC_THREAD_DESTRUCTOR+ITERATIONS
PTHREAD_KEYS_MAX 线程可以创建的键的最大数目(12.6) _SC_THREAD_KEYS_MAX
PTHREAD_STACK_MIN 一个线程的栈可用的最小字节数(12.3) _SC_THREAD_STACK_MIN
PTHREAD_THREADS_MAX 进程可以创建的最大线程数(12.3) _SC_THREAD_THREADS_MAX

12.3 线程属性

视频部分的笔记也可以参考看看

pthrad接口为我们提供了每个对象关联不同属性来细调线程和同步对象的行为。通常,管理属性的函数遵循相同的模式

  1. 每个对象与它自己类型的属性对象进行关联(线程与线程属性,互斥量与互斥量属性)。一个属性对象可以代表多个属性(意思是属性对象是一个结构,它包含多个属性的意思吗?)。属性对象对应用程序是不透明的,通过函数来管理这些属性对象
  2. 有一个初始化函数,把属性设置为默认值
  3. 还有一个销毁属性对象的函数。负责释放资源
  4. 每个属性都有一个属性对象中获取属性值的函数。
  5. 每个属性都一个设置属性值的函数。
1
2
3
4
#include <pthread.h>
int pthread_attr_init(pthread_attr_t* attr);
int pthread_attr_destroy(pthread_attr_t* attr);
//成功返回0,否则返回错误编号

分离线程,如果对现有的某个线程的终止状态不关心,可以使用pthread_detach函数让操作系统再线程退出时收回它所占有的资源。可以修改pthread_attr_t中的detachastate线程属性,让线程一开始就处于分离状态。可以使用pthread_attr_setdetachstate函数把线程属性detachstate设置成以下两个合法值之一: PTHREAD_CREATE_DETACHED,以分离状态启动线程,或者PTHREAD_CREATE_JOINABLE,正常启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <pthread.h>
int pthread_attr_getdetachstate(const pthread_attr_t* restrict attr, int* detachstate);
int pthread_attr_setdetachstate(pthread_attr_t* attr, int detachstate);
//成功返回0,否则返回错误编号
//示例
#include "apue.h"
#include <pthread.h>

int makethread(void*(*fn)(void*), void* arg) {
int err;
pthread_t tid;
pthread_attr_t attr;

err = pthread_attr_init(&attr);
if (err != 0)
return err;

err = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
if (err == 0)
err = pthread_create(&tid, &attr, fn, arg);
pthread_attr_destroy(&attr);

return err;
}

线程栈属性,不一定所有的操作系统都支持线程栈属性。可以在编译阶段使_POSIX_THREAD_ATTR_STACKADDR和 _POSIX_THREAD_ATTR_STACKSIZE符号来检查系统是否支持每一个线程栈属性。也可以通过sysconf函数检查

1
2
3
4
#include <pthread.h>
int pthread_attr_getstack(const pthread_attr_t* restrict attr, void** restrict stackaddr, size_t* restrict stacksize);
int pthread_attr_setstack(pthread_attr_t* attr, void* stackaddr, size_t stacksize);
//成功返回0,否则返回错误编号

用途的话,简单来说,一个进程中的虚拟空间中,只有一个栈,被线程共享,如果不够了的话,可以通过设置线程栈属性改变它的位置,通过malloc和mmap创建到堆上去。视频笔记有使用例子,一个在堆上不断创建线程的例子

1
2
3
4
#include <pthread.h>
int pthread_attr_getstacksize(const pthread_attr_t* restrict attr, size_t* restrict stacksize);
int pthread_attr_setstacksize(pthread_attr_t* attr, size_t stacksize);
//成功返回0,否则返回错误编号

如果希望改变默认栈大小,又不想自己处理线程栈的分配问题,可以用这个属性。设置stacksize属性时,选择的大小不能小于PTHREAD_STACK_MIN

1
2
3
4
#include <pthread.h>
int pthread_attr_getguardsize(const phtread_attr_t* restrict attr, size_t* restrict guardsize);
int pthread_attr_setguardsize(pthread_attr_t* attr, size_t guardsize);
//成功返回0,否则返回错误编号

线程属性guardsize控制着线程栈末尾之后用来避免栈溢出的扩展内存大小。默认值由实现决定,常用值是系统页大小。如果修改了线程属性的stackaddr,系统就认为我们自己管理栈,进而使栈警戒缓冲区机制无效,这等同于把guardsize设置为0。

如果guardsize被修改了,操作系统可能会把它取到页大小的整数倍

12.4 同步属性

1.互斥量属性

初始化和反初始化函数

1
2
3
4
#include <pthread.h>
int pthread_mutexattr_init(pthread_mutexattr_t* attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t* attr);
//若成功返回0,否则返回错误编号

值得注意的3个属性: 进程共享属性,健壮属性以及类型属性 , 进程共享属性为可选的,注意系统中是否定义了_POSIX_THREAD_PROCESS_SHARED符号来判断平台是否支持这个属性

在进程中,多个线程可以访问同一个同步对象。这是默认的行为,在这种情况下,进程共享互斥量属性设置为PTHREAD_PROCESS_PRIVATE

存在这样的机制:允许相互独立的多个 进程把同一个内存数据块映射到它们各自独立的地址空间中。就像多个线程访问共享数据一样,多个进程访问共享数据通常也需要同步。如果进程设置互斥量属性为PTHREAD_PROCESS_SHARED,就可以满足这样的需求

1
2
3
4
#include <pthread.h>
int pthread_mutexattr_getpshared(const pthread_mutexattr_t* restrict attr, int* restrict pshared);
int pthread_mutexattr_setpshared(pthread_mutexattr_t* attr, int pshared);
//成功返回0,否则返回错误编号

健壮属性

1
2
3
4
#include <pthread.h>
int pthread_mutexattr_getrobust(const pthread_mutexattr_t* restrict attr, int* restrict robust);
int pthread_mutexattr_setrobust(pthread_mutexattr_t* attr, int roubust);
//成功返回0,否则返回错误编号

有一说一没看懂是个什么意思,反正两种可能,默认值是PTHREAD_MUTEX_STALLED,意味着互斥量的进程终止时不需要采取特别的动作。看不懂,感觉描述有问题,而且没例子,以后遇到了再回过头了理解

类型互斥量属性

PTHREAD_MUTEX_NORMAL 一种标准互斥量类型,不做任何特殊的错误检查或死锁检测
PTHREAD_MUTEX_ERRORCHECK 此互斥量类型提供错误检测
PTHREAD_MUTEX_RECURSIVE 此互斥量类型允许同一线程在互斥量解锁之前对该互斥量进行多次加锁。递归互斥量维护锁的计数,在解锁次数和加锁次数不相同的情况下,不会释放锁。所以,如果对一个递归互斥量加锁两次,然后解锁一次,那么这个互斥量仍然处于加锁状态,对它再次解锁以前不能释放该锁
PTHREAD_MUTEX_DEFAULT 此互斥量类型可以提供默认特性和行为。操作系统在实现它的时候可以把这种类型自由地映射到其他互斥量类型中的一种(自定义锁?)
互斥量类型 没有解锁时重新加锁 不占用时解锁 在已解锁时解锁
PTHREAD_MUTEX_NORMAL 死锁 未定义 未定义
PTHREAD_MUTEX_ERRORCHECK 返回错误 返回错误 返回错误
PTHREAD_MUTEX_RECURSIVE 允许 返回错误 返回错误
PTHREAD_MUTEX_DEFAULT 未定义 未定义 未定义
1
2
3
4
#include <pthread.h>
int pthread_mutexattr_gettype(const pthread_mutexattr_t* restrict attr, int* restrict type);
int pthread_mutexattr_settype(pthread_mutexattr_t* attr, int type);
//成功返回0,否则返回错误编号

使用递归锁的情况可能是,在不改变单线程函数接口的情况下,解决并发问题,参考下面两图第一张图,使用递归锁,第二张图不使用

image-20221101203410745

image-20221101203424061

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
//使用递归互斥量的一个示例情况  也没看懂
#include "apue.h"
#include <pthread.h>
#include <time.h>
#include <sys/time.h>

extern int makethread(void*(*)(void*), void*);

struct to_info {
void (*to_fn)(void*); /* function */
void* to_arg /* argument */
struct timespec to_wait /* time to wait */
};

#define SECTONSEC 1000000000 /* seconds to nanoseconds */

#if !defined(CLOCK_REALTIME) || defined(BSD)
#define clock_nanosleep(ID, FL, REQ, REM) nanosleep((REQ), (REM))
#endif

#ifnedf CLOCK_REALTIME
#define CLOCK_REALTIME 0
#define USECTONSEC 1000 /* microseconds to nanoseconds */

void clock_gettime(int id, struct timespec* tsp) {
struct timeval tv;

gettimeofday(&tv, NULL);
tsp->tv_sec = tv.tv_sec;
tsp->tv_nsec = tv.tv_usec * USECTONSEC;
}
#endif

void* timeout_helper(void* arg) {
struct to_info* tip;

tip = (struct to_info*)arg;
clock_nanosleep(CLOCK_REALTIME, 0, &tip->to_wait, NULL);
(*tip->to_fn)(tip->to_arg);
free(arg);
return 0;
}

void timeout(const struct timespec* when, void(*func)(void*), void* arg) {
struct timespec now;
struct to_info* tip;
int err;

clock_gettime(CLOCK_REALTIME, &now);
if ((when->tv_sec > now.tv_sec) ||
(when->tv_sec == now.tv_sec && when->tv_nsec > now.tv_nsec)) {
tip = malloc(sizeof(struct to_info));
if (tip != NULL) {
tip->to_fn = func;
tip->to_arg = arg;
tip->to_wait.tv_sec = when->tv_sec - now.tv_sec;

if (when->tv_nsec >= now.tv_nsec) {
tip->to_wait.tv_nsec = when->tv_nsec - now.tv_nsec;
} else {
tip->to_wait.tv_sec--;
tip->to_wait.tv_nsec = SECTONSEC - now.tv_nsec + when->tv_nsec;
}
err = makethread(timeout_healper, (void*)arg);
if (err == 0)
return;
else
free(tip);
}
}

(*func)(arg);
}

pthread_mutexattr_t attr;
pthread_mutex_t mutex;

void retry(void* arg) {
pthread_mutex_lock(&mutex);
/* perform retry steps ... */
pthread_mutex_unlock(&mutex);
}

int main() {
int err, condition, arg;
struct timespec when;

if ((err = pthread_mutexattr_init(&attr)) != 0)
err_exit(err, "pthread mutexarttr init error");
if ((err = pthread_mutexattr_settype(&atrr, PTHREAD_MUTEX_RECURSIVE)) != 0)
err_exit(err, "can't set recursive type");
if ((err = pthread_mutex_init(&mutex, &attr)) != 0)
err_exit(err, "can't create recursive mutex");

/* continue processing ... */

pthread_mutex_lock(&mutex);

/* check the condition under the protection of a lack to
* make the check and the call to timeout atomic
*/

if (condition) {
/* Calculate the absolute time when we want to retry */

clock_gettime(CLOCK_REALTIME, &when);
when.tv_sec += 10;
timeout(&when, retry, (void*)((unsigned long)arg));
}
pthread_mutex_unlock(&mutex);

exit(0);
}
2.读写锁属性

只支持进程共享属性,与互斥量的进程共享属性是相同的

1
2
3
4
5
6
7
#include <pthread.h>
int pthread_rwlockattr_init(pthread_rwlockattr_t* attr);
int pthread_rwlockattr_destroy(pthread_rwlockattr_t* attr);
//成功返回0,否则返回错误编号
#include <pthread.h>
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t* restrict attr, int* restrict pshared);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t* attr, int pshared);
3.条件变量属性

一般定义了两个属性:进程共享属性和时钟属性

1
2
3
4
#include <pthread.h>
int pthread_condattr_init(pthread_condattr_t* attr);
int pthread_condattr_destroy(pthread_condattr_t* attr);
//成功返回0,否则返回错误编号

与其他同步属性一样,条件变量支持进程控制属性。它控制着条件变量是可以被单进程的多个线程使用,还是可以被多进程的线程使用,

1
2
3
4
#include <pthread.h>
int pthread_condattr_getpshared(const pthread_condattr_t* restrict attr, int* restrict pshared);
int pthread_condattr_setpshared(pthread_condattr_t* attr, int pshared);
//成功返回0,否则返回错误编号

时钟属性控制计算pthread_cond_timedowait函数的超时参数时采用的是哪个时钟

1
2
3
4
#include <pthread.h>
int pthread_condattr_getclock(const pthread_condattr_t* restrict attr, clockid_t* restrict clock_id);
int pthread_condattr_setclock(pthread_condattr_t* attr, clockid_t clock_id);
//成功返回0,否则返回错误编号
4.屏障属性

同样的初始化和反初始化函数

1
2
3
4
#include <pthread.h>
int pthread_barrierattr_init(pthread_barrierattr_t* attr);
int pthread_barrierattr_destroy(pthread_barrierattr_t* attr);
//成功返回0,否则返回错误编号

屏障也只有进程共享属性,它控制着屏障是可以被多进程的线程使用,还是只能被初始化屏障内的进程的多线程使用

1
2
3
4
#include <pthread.h>
int pthread_barrierattr_getpshared(const pthread_barrierattr_t* restrict attr, int* restrict pshared);
int pthread_barrierattr_setpshared(pthread_barrierattr_t* attr, int pshared);
//成功返回0,否则返回错误编号

可选值还是那两个 PTHREAD_PROCESS_SHARED 和 PTHREAD_PROCESS_PRIVATE

12.5 重入

在这两种情况下,多个控制线程在相同的时间有可能调用的函数。线程安全和可重入。 这里提到了以下标准下线程安全也就是可重入的函数,以及对于不可重入的一些函数的可重入版本。P355

对于线程的可重入概念和信号处理程序的可重入不一样。如果函数对异步信号处理程序的重入是安全的,那么就可以说函数是异步信号安全的。

这里还提到了一些以线程安全管理FILE对象的方法,可使用flockfile和ftrylockfile获取给定的FILE对象关联的锁,这里看不太懂,用到的时候再看

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int ftrylockfile(FILE* fp);
//成功返回0,若不能获取锁返回非0
void flockfile(FILE* fp);
void funlockfile(FILE* fp);

//不加锁版本的基于字符的标准I/O例程
int getchar_unlocked(void);
int getc_unlocked(FILE* fp);
//成功返回下一个字符,遇到文件尾或出错返回EOF
int putchar_unlocked(int c);
int puc_unlocked(int c, FILE* fp);
//成功返回c,出错返回EOF
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
//getenv(7.9节)的一个可能实现   非可重入版本
#include <limits.h>
#include <string.h>

#define MAXSTRING32 4096

static char envbuf[MAXSTRING32];

extern char** environ;

char* getenv(const char* name) {
int i, len;

len = strlen(name);
for (i = 0; environ[i] != NULL; i++) {
if ((strncmp(name, environ[i], len) == 0) && (environ[i][len] == '=')) {
strncpy(envbuf, &environ[i][len+1], MAXSTRING32-1);
return envbuf;
}
}
return NULL;
}

//可重入版本 getenv_r。它使用pthread_once函数来确保不管多少线程同城竞争调用getenv_r,每个进程只调用treahd_init函数一次

#include <string.h>
#include <errno.h>
#include <pthread.h>
#include <stdlib.h>

extern char** environ;
pthread_mutex_t env_mutex;
static pthread_once_t init_done = PTHREAD_ONCE_INIT;

static void thread_init() {
pthread_mutexattr_t attr;

pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); //递归锁
pthread_mutex_init(&env_mutex, &attr);
pthread_mutexattr_destroy(&attr);
}

int getenv_r(const char* name, char* buf, int buflen) {
int i, len, olen;

pthread_once(&init_done, thread_init);
len = strlen(name);
pthread_mutex_lock(&env_mutex);
for (i = 0; environ[i] != NULL; i++) {
if ((strncmp(name, environ[i], len) == 0) && (environ[i][len] == '=')) {
olen = strlen(&environ[i][len+1]);
if (olen >= buflen) {
pthread_mutex_unlock(&env_mutex);
return ENOSPC;
}
strcpy(buf, &environ[i][len+1]);
pthread_mutex_unlock(&env_mutex);
return 0;
}
}
pthread_mutex_unlock(&env_mutex);
return ENOENT;
}

12.6 线程特定数据

存储和查询某个特定线程相关数据的一种机制。

在分配线程特定数据之前,需要创建与该数据关联的键,这个键将用于获取对线程特定数据的访问

1
2
3
#include <pthread.h>
int pthread_key_create(pthread_key_t* keyp, void(*destructor)(void*));
//成功返回0,否则返回错误编号

对所有的线程可以通过调用pthread_key_delete取消键与线程特定数据之间的关联,但调用它并不会激活与键关联的析构函数。

1
2
3
#include <pthread.h>
int pthread_key_delete(pthread_key_t key);
//成功返回0,否则返回错误编号

需要确保分配的键并不会由于初始化阶段的竞争而发生变动,入下面函数可能会导致两个线程都掉用key_create

1
2
3
4
5
6
7
8
9
10
11
void destructor(void*);

pthread_key_t key;
int init_done = 0;

int threadfunc(void* arg) {
if (!init_done) {
init_done = 1;
err = pthread_key_create(&key, destructor);
}
}

有些线程可能看到一个键值,其他线程可能看到另一个不同的键值,这取决于系统的调度,解决这种的竞争的方法通过pthread_once 感觉mutex上锁也行

1
2
3
4
#include <pthread.h>
pthread_once_t initflag = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t* initflag, void(*initfn)(void));
//成功返回0,否则返回错误编号

initflag必须是一个非本地变量(全局变量或静态变量),而且必须初始化为PTHREAD_ONCE_INIT

1
2
3
4
5
6
7
8
9
10
11
12
//正确示例
void destructor(void*);

pthread_key_t key;
pthread_once_t init_done = PTHREAD_ONCE_INIT;
void thread_init() {
err = pthread_key_create(&key, destructor);
}

int threadfunc(void* arg) {
pthread_once(&init_done, thread_init);
}

键创建后,就可以通过调用setspecific函数把键和线程特定数据关联起来,通过getspecific获取特定数据的地址

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
#include <pthread.h>
void* pthread_getspecific(pthread_key_t key);
//返回线程特定数据值,如果没有值与key关联,返回NULL
int pthread_setspecific(pthread_key_t key, const void* value);
//成功返回0,否则返回错误编号

//同样是getenv的一个实现,但这次不改变getenv的接口,前一节通过改变接口实现线程安全
#include <limits.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>

#define MAXSTRINGSZ 4096

static pthread_key_t key;
static pthread_once_t init_done = PTHREAD_ONCE_INIT;
pthread_mutex_t env_mutex = PTHREAD_MUTEX_INITIALIZER;

extern char** environ;

static void thread_init() {
pthread_key_create(&key, free);
}

char* getenv(const char* name) {
int i, len;
char* envbuf;

pthread_once(&init_done, thread_init);
pthread_mutex_lock(&env, mutex); //这里应该可以用读写锁,增加读时并发性
envbuf = (char*)pthread_getspecific(key);
if (envbuf == NULL) {
envbuf = malloc(MAXSTRINGSZ);
if (envbuf == NULL) {
pthread_mutex_unlock(&env_mutex);
return NULL;
}
pthread_setspecific(key, envbuf);
}

len = strlen(name);
for (i = 0; environ[i] != NULL; i++) {
if ((strncmp(name, environ[i], len) == 0) && (environ[i][len] == '=')) {
strncpy(envbuf, &environ[i][len+1], MAXSTRINGSZ-1);
pthread_mutex_unlock(&env_mutex);
return envbuf;
}
}
pthread_mutex_unlock(&env_mutex);
return NULL;
}

注意虽然这个版本的getenv是线程安全的,但它并不是异步信号安全的。对信号而言他不是可重入的,因为调用了malloc

12.7 取消选项

有两个属性没有包含在pthread_attr_t结构中,可取消状态可取消类型。这两个属性影响着线程在响应pthread_cancel函数时所呈现的行为

1
2
3
4
//可取消状态属性可以是PTHREAD_CANCEL_ENABLE 或 PTHREAD_CANCEL_DISABLE
#include <pthread.h>
int pthread_setcancelstate(int state, int* oldstate);
//成功返回0,否则返回错误编号

调用pthread_cancel函数并不等待线程终止,他会继续允许,直到到达某个取消点。

线程默认的可取消状态是PTHREAD_CANCEL_ENABLE,当状态设为PTHREAD_CANCEL_DISABLE时,对pthread_cancel的调用不会杀死线程。当状态变为PTHREAD_CANCEL_ENABLE时被处理,看起来应该是起到了一个延迟的作用,比如我们希望cancel后也能执行一部分行为再自己被取消

1
2
3
#include <pthread.h>
void pthread_testcancel();
//手动添加取消点

我们之前描述的这种默认的取消类型也称为推迟取消。线程在到达取消点之前,不会出出现真正的取消,通过pthread_setcanceltype可以修改取消类型

1
2
3
#include <pthread.h>
int pthread_setcanceltype(int type, int* oldtype);
//成功返回0,否则返回错误编号

参数类型: PTHREADCANCEL_DEFERRED 或 PTHREAD_CANCEL_ASYNCHRONOUS

12.8 线程和信号

1
2
3
#include <signal.h>
int pthread_sigmask(int how, cosnt sigset_t* restrict set, sigset_t* restrict oset);
//成功返回0,否则返回错误编号

用法基本和sigprocmask一样,但只对当前线程生效

1
2
3
4
//线程可以通过调用sigwait等待一个或多个信号的出现
#include <signal.h>
int sigwait(const sigset_t* restrict set, int* restrict signop);
//成功返回0,否则返回错误编号

把信号发送给线程,pthread_kill

1
2
3
#included <signal.h>
int pthread_kill(pthread_t thread, int signo);
//成功返回0,否则返回错误编号
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
//示例
#include "apue.h"
#include <pthread.h>

int quitflag; /* set nonzero by thread */
sigset_t mask;

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t waitloc = PTHREAD_COND_INITIALIZER;

void* thr_fn(void* arg) {
int err, signo;

for (;;) {
err = sigwait(&mask, &signo);
if (err != 0)
err_exit(err, "sigwait error");

switch (signo) {
case SIGINT:
printf("\ninterrupt\n");
break;
case SIGQUIT:
pthread_mutex_lock(&lock);
quitflag = 1;
pthread_mutex_unlock(&lock);
pthread_cond_signal(&waitloc);
return 0;
default:
printf("unexpected signal %d\n", signo);
exit(1);
}
}
}

int main() {
int err;
sigset_t oldmask;
pthread_t tid;

sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGQUIT);
if ((err = pthread_sigmask(SIG_BLOCK, &mask, &oldmask)) != 0)
err_exit(err, "SIG_BLOCK error");

err = pthread_create(&tid, NULL, thr_fn, 0);
if (err != 0)
err_exit(err, "can't create thread");

pthread_mutex_lock(&lock);
while (quitflag == 0)
pthread_cond_wait(&waitloc, &lock);
pthread_mutex_unlock(&lock);

/* SIGQUIT has been caught and is now blocked; do whatever */
quitflag = 0;

/* reset signal mask which unblocks SIGQUIT */
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
err_sys(err, "error");

exit(0);
}

12.9 线程和fork

12.10 线程和I/O

12.11 小结