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 / 查看目录 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 用绝对路径 chmod [who] [+|-|=] [mode] 修改文件权限 chmod 777 xxx chown xxx ttt 将ttt的所属者改为xxx chown xxx:yyy ttt 将ttt的所属者和所属组改为 xxx yyy chgrp 修改所属组 find /home/itcast/ -name "hel?" find 查找目录 -size +10 k find 查找目录 -type d/f/b/c/s/p/l grep -r "查找内容" 查找路径 ficonfig Nslookup
上图为 -l 详细显示的信息的解释图
常见文件 1 2 3 4 5 6 7 8 9 /bin 命令 /dev 设备文件 /etc 配置文件信息 /home 所有用户的目录 /lib 存的一些动态库 /media 自动挂载库 /mnt 手动挂载 /root 管理员目录 /usr 当前用户软件安装目录
文件或目录熟悉
安装和卸载 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 清除所有软件包 sudo dpkg -i xxx.deb sudo dpkg -r xxx
压缩
1 2 3 tar jxvf 压缩包名字(解压到当前目录) tar jxvf 压缩包名字 -C 压缩的目录
zip压缩目录需要-r
管道
用户管理
三种服务器搭建 ftp服务器
nfs服务器 (共享文件夹)
ssh服务器
vim 操作
分屏: 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 包含调试信息 -0 n (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
动态库 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 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完后,更新配置
c)
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 1 ) set follow-fork-mode child 2 ) set follow-fork-mode parent
Makefile
虚拟地址空间 本质上类似虚拟地址到实际内存地址的映射,方便将零散的空间,构成一个连续的整体,和deque的状态有点像
stat lstat区别,穿透和不穿透
程序和进程 CPU基本运作方式
MMU
进程控制块 PCB
进程状态
环境变量函数
FORK 1 2 3 4 5 6 for (int i=0 ; i<n; i++){ if (fork() == 0 ) break ; }
进程共享 特点:读时共享,写时复制
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 wait (status) : 返回 成功:pid 失败:-1 status: 传出参数 1. 阻塞等待子进程 2. 回收子进程资源 3. 获取子进程结束状态 利用宏获取状态: 1. if WIFEXITED (status) (真) WEXITSTATUS (status) ; 2. if WIFSIGNALED (status) (真) WTERMSIG (status) ; 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
管道:使用简单
FIFO:非血缘关系间
信号:开销小
共享内存:非血缘关系间
本地套接字
管道
获取管道缓冲区大小 : 函数:fpathconf2 参2: __PC_PIPE_BUF 命令 : ulimit -a
管道优劣: 优 : 实现简单 缺 : 单向通信 血缘关系only
FIFO mkfifo函数和命令
共享存储映射
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 );
strace strace + 可执行文件, 追踪程序执行过去中使用的系统调用,就比如利用程序进行进程通信时,底层其实还是使用mmap实现的通信
小实验 利用fifo实现简单的本地聊天室
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 #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) ;#include "server.h" #define BUFSIZE 1068 void init_server () { 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 ; } #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 () ;#include "client.h" #define BUFSIZE 1068 void init_client () { login_server (); link_flag = 1 -link_flag; 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); 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) 每个进程有且只有唯一一个定时器
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) ; int sigfillset (sigset_t *set) ; int sidaddset (sigset_t *set, int signum) ; int sigdeleset (sigset_T *set,int signum) ; int sigismemeber (const sigset_t *set,int signum) ; 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 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 int sigaction (int signum, const struct sigaction* act, struct sigaction* oldact) ; act传入参数, oldact传出参数 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 )
时序竞态 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 #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 ; } #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; newact.sa_handler = catch_sigalrm; sigemptyset (&newact.sa_mask); newact.sa_flags = 0 ; sigaction (SIGALRM, &newact, &oldact); sigemptyset (&newmask); sigaddset (&newmask, SIGALRM); sigprocmask (SIG_BLOCK, &newmask, &oldmask); alarm (seconds); suspmask = oldmask; sigdelset (&suspmask, SIGALRM); 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 ); do_sig_parent (0 ); while (1 ) { ; 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 ) { ; if (flag == 1 ) { kill (getppid (), SIGUSR2); flag = 0 ; } } } return 0 ; }
可重入函数/不可重入函数 1 2 3 4 5 6 7 8 1. 定义可重入函数,函数内不能含有全局变量及static 变量,不能使用malloc、free2. 信号捕捉函数应设计为可重入函数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后唤醒时 #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 ; } 1. 子进程继承了父进程的信号屏蔽字和信号处理动作,但子进程没有继承未决信号集2. 注意注册信号捕捉函数的位置3. 应该在fork之前, 阻塞SIGCHLD信号, 注册完捕捉函数后解除阻塞
信号传参 1 2 3 4 5 6 7 8 9 10 11 12 13 int sigqueue (pid_t pid, int sig, const union sigval value) ;union sigval { int sival_int; void *sival_ptr; }; 就是上述sigaction 中与sa_handler组成union 的另一成员 void (*sa_sigaction)(int , siginfo_t *.void *);
中断系统调用
终端 1 2 3 4 5 6 7 终端即输入输出设备的总称 init --> fork --> exec --> getty --> 用户输入帐号 --> login --> 输入密码 --> exec --> bash
进程组 1 2 3 4 5 6 7 8 9 pid_t getpgrp (void ) ; pid_t getpgid (pid_t pid) ; 成功:0 失败:-1 ,设置errno int setpgid (pid_t pid, pid_tgpid) 非root进程只能改变自己创建的子进程,或有权操作的进程
会话 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 1. 调用进程不能是进程组组长,该进程变成新会话首进程(session header) 2. 该进程成为一个新进程组的组长进程。 3. 需有root权限(ubuntu不需要) 4. 新会话丢弃原有的控制终端,该会话没有控制终端 5. 该调用进程是组长进程,则出错返回 6. 建立新会话时,先调用fork, 父进程终止,子进程调用setsid pid_t getisid (pid_t pid) 成功:返回调用进程的会话ID;失败:-1,设置errno 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 ; }
线程
Linxu内核线程的实现原理 1 2 3 4 5 6 7 1. 轻量级进程(light-weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone2. 从内核里看进程和线程是一样的,都有各自不同的PCB,但是PCB中指向内存资源的三级页表是相同的3. 进程可以蜕变成线程4. 线程可看做寄存器和栈的集合5. 在linux下,线程最是小的执行单位;进程是最小的分配资源单位
线程共享/非共享资源 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 1. 文件描述符表2. 每种信号的处理方式3. 当前工作目录4. 用户ID和组ID5. 内存地址空间(.text/.data/.bss/heap/共享库) 1. 线程ID2. 处理器现场和栈指针(内核栈)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 pthread_t pthread_self (void ) ; int pthread_create (pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void * arg) ;1. 参数一:传出参数,传出分配的线程ID2. 参数二:通常传NULL ,表示线程使用默认属性, 想用具体属性可以修改该参数3. 参数三:函数指针,指向线程的主函数,函数运行结束,即线程结束4. 线程主函数执行期间,使用的参数void pthread_exit (void * retval) ; int pthread_join (pthread_t thread, void **retval) ;对比 进程: main返回值, exit参数->int , 等待子进程结束 wait函数参数->int * 线程: 线程主函数返回值, pthread_exit->void *, 等待线程结束 pthread_join 参数->void ** int pthread_detach (pthread_t thread) ;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 int pthread_equal (pthread_t t1, pthread_t t2) ;1. 线程主函数return 2. 调用pthread_cancel3. 调用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) ;int pthread_attr_setdetachstate (pthread_attr_t *attr, int detachstate) ;int pthread_attr_getdetachstate (pthread_attr_t *attr, int *detachstate) ;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_exit2. 避免僵尸线程 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;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 ); 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); tid = (pthread_t *)malloc (sizeof (pthread_t ) * (n + 1 )); if (tid == NULL ) err_sys ("fail to malloc" ); done = (int *)calloc (sizeof (int ), n); if (done == NULL ) err_sys ("fail to malloc" ); 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 int pthread_mutex_init (pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr) ; 参1 :&mutex 参2 :互斥量属性 通常传null 静态初始化: pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; int pthread_mutex_destroy (pthread_mutex_t *mutex) ;int pthread_mutex_lock (pthread_mutex_t *mutex) ;int pthread_mutex_trylock (pthread_mutex_t *mutex) ;int pthread_mutex_unlock (pthread_mutex_t *mutex) ;
加锁与解锁 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 #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 int pthread_rwlock_init (pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr) ; 参2 :读写锁属性, 默认Null int pthread_rwlock_destroy (pthread_rwlock_t *rwlock) ;int pthread_rwlock_rdlock (pthread_rwlock_t *rwlock) ; 读锁lock int pthread_rwlock_wrlock (pthread_rwlock_t *rwlock) ; 写锁lock int pthread_rwlock_tryrdlock (pthread_rwlock_t *rwlock) ; 非阻塞读锁lock int pthread_rwlock_trywrlock (pthread_rwlock_t *rwlock) ; 非阻塞写锁lock int pthread_rwlock_unlock (pthread_rwlock_t *rwlock) ;#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 int pthread_cond_init (pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr) ; 参2 :条件变量属性, 通常传NULL 静态初始化: pthread_cond_t cond = PTHREAD_COND_INITIALIZER; int pthread_cond_destroy (pthread_cond_t *cond) ;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返回时, 解除阻塞并重新申请互斥锁 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; 秒 long tv_nsec; 纳秒 } 形参abstime:绝对时间。 如:time (NULL )返回的就是绝对时间。而alarm (1 )是相对时间,相对当前时间定时1 秒钟。 struct timespec t = {1 , 0 }; pthread_cond_timedwait (&cond, &mutex, &t); 只能定时到 1970 年1 月1 日 00 :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; 秒 suseconds_t tv_usec; 微秒 }; int pthread_cond_signal (pthread_cond_t *cond) ; 唤醒至少一个阻塞在条件变量上的线程 int pthread_cond_broadcast (pthread_cond_t *cond) ; 唤醒所有 #include <stdlib.h> #include <unistd.h> #include <pthread.h> #include <stdio.h> 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 实现对用户隐藏, +--通过函数实现 int sem_init (sem_t *sem, int pshared, unsigned int value) ; 参1 :sem信号量 参2 :取0 用于线程间, 取非0 用于进程间 参3 :value指定信号量初值 int sem_destroy (sem_t *sem) ;int sem_wait (sem_t *sem) ; 加锁lock int sem_trywait (sem_t *sem) ; 类比trylock 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); 传参int sem_post (sem_t *sem) ; 解锁unlock #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 #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, ...) ; 参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_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, ... ) ;int openat (int fd, const char *path, int oflag, ... ) ; 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
目的:
让线程使用相对路径打开目录的文件, 同一进程中的所有线程共享相同的当前工作目录, 因此很难让同一进程的多个不同线程在同一时间工作在不同的目录
可以避免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) ; open (path, O_WRONLY | O_CREATE | O_TRUNC, mode);
另一个不足之处, 在于create只能创建只写打开的文件
3.5 函数close 1 2 3 #include <unistd.h> int close (int fd) ;
关闭一个文件时还会释放该进程加在该文件上的所有记录锁,当一个进程终止时, 内核自动关闭所有它打开的文件
3.6 函数lseek 1 2 3 #include <unistd.h> off_t lseek (int fd, off_t offset, int whence) ;
对参数offset 的解释与参数whence 的值有关:
若whence是SEEK_SET, 将文件的偏移量设置为距文件开始offset个字节
若whence是seek_cur, 将文件的偏移量设置为当前值加offset, offset可正可负
若whence是SEEK_END, 将文件的偏移量设置为文件长度加offset, 可正可负
1 2 3 4 off_t currpos;currpos = lseek (fd, 0 , SEEK_SET);
文件偏移量可以大于文件的当前长度, 这种情况下会形成空洞文件,但位于文件中没有写过的字节都被读成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" ); if (lseek (fd, 16384 , SEEK_SET) == -1 ) err_sys ("lseek error" ); if (write (fd, buf2, 10 ) != 10 ) err_sys ("buf2 write error" ); exit (0 ); }
3.7 函数read 1 2 3 #include <unistd.h> ssize_t read (int fd, void * buf, size_t nbytes) ;
有多种情况可使实际读到的字节数少于要求读的字节数:
读普通文件时, 在读到要求的字数之前已到达文件尾端, 例如文件30字节, 而要求读100字节, 则read返回30, 下一次调用返回0
从终端设备读时,通常一次最多读一行
从网络读时, 取决于网络中的缓冲机制
从管道和FIFO读时, 返回实际读到的可用字节数
从某些面向记录的设备时(如磁带), 一次最多返回一个记录
当一信号造成中断时, 而已读了部分数据
3.8 函数write 1 2 3 #include <unistd.h> ssize_t write (int fd, const void *buf, size_t nbytes) ;
对于普通文件, 写操作从文件的当前偏移量处开始,如果在打开文件时,指定了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
大多数文件系统为改善性能采用某种预读技术
3.10 文件共享 本小节分两部分,第一部分描述了内核中文件是以何等方式打开文件
每个进程在进程表中都有一个记录项,记录项包含一张打开的文件描述符表,每个描述符占用一项,与每个文件描述符关联的是
a.文件描述符标志(close_on_exec)
b.指向一个文件表项的指针
内核为所有打开文件维持一张表,每个表项包含:
a.文件状态标志(读,写,添加,同步和非阻塞等)
b.当前文件偏移量
c.指向该文件v节点表项的指针
每个打开文件都有一个v节点结构,v节点包含了文件类型和对此文件进行各种操作函数的指针,对于大多数文件还包含i节点,索引节点
另一部分说明了,如果两个独立进程各自打开了同一文件:
我们假定第一个进程在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 #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) ;
3.12 函数dup和dup2 1 2 3 4 5 #include <unistd.h> int dup (int fd) ;int dup2 (int fd, int fd2) ;
dup返回的新文件描述符一定是当前可用文件描述符中的最小值,对于dup2,可用用fd2指定新文件描述符的值,如果fd2已经打开,先将其关闭。如果fd等于fd2,则dup2返回fd2,而不关闭它。否则,fd2的FD_CLOEXEC文件描述符标志就被清除(?不理解什么意思)
1 2 3 4 dup (fd); -----> fcntl (fd, F_DUPFD, 0 );dup2 (fd, fd2);-----> close (fd2); fcntl (fd, F_DUPFD, fd2);
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) ; void sync (void ) ;
sync只是将修改过的块缓冲区排入写队列,然后返回,不等待实际写磁盘操作结束,被称为update的守护进程,会周期性调用该函数
fsync只对fd指定的文件起作用,并且等待实际写硬盘操作结束
fdtasync与fsync类似,但它只影响文件的数据部分,而除数据外,fsync还会同步更新文件属性
3.14 函数fcntl 1 2 3 #include <fcntl.h> int fcntl (int fd, int cmd, ... ) ;
fcntl函数有以下5个功能:
复制一个已有的描述符(cmd = F_DUPFD 或 F_DUPFD_CLOEXEC)
获取/设置文件描述符标志(cmd = F_GETFD 或 F_SETFD)
获取/设置文件状态标志(cmd = F_GETFL 或 F_SETFL)
获取/设置异步I/O所有权(cmd = F_GETOWN 或 F_SETOWN)
获取/设置记录锁 (参考上面视频笔记部分的文件锁,很详细,后续也会继续补全)
该小节只说明前四种,记录锁在后面章节补充:
**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时已经描述了文件状态
对于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> 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" ); } 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) ;
四个函数功能基本一样,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 { mode_t st_mode; ino_t st_ino; dev_t st_dev; dev_t st_rdev; nlink_t st_nlink; uid_t st_uid; gid_t st_gid; off_t st_size; struct timespec st_atime; struct timespec st_mtime; struct timespec st_ctime; blksize_t st_blksize; blkcnt_t st_blocks; } struct timespec { time_t tv_sec; long tv_nsec; }
4.3 文件类型 UNXI系统中文件的七种类型
**普通文件:**最常用的文件,包含某种形式的数据,至于是文本还是二进制,对于UNXI内核并无区别
目录文件: 只有内核可以之间写目录,进程必须使用当前章节的介绍的函数才可以更改目录
**块特殊文件:**这种类型的文件提供对设备带缓冲的访问,每次访问以固定长度为单位进行
**字符特殊文件:**这种类型的年纪提供对设备不带缓冲的访问,每次访问长度可变,系统中的所有设备要么是块特殊文件,要么是字符特殊文件
**FIFO:**命名管道,用于进程间通信,上面视频笔记小实验聊天室用到了
**套接字:**用于进程间的网络通信,也可以用于在一台宿主机上的非网络通信,16章
**符号链接:**这种类型文件指向另一个文件,类似快捷方式
1 2 3 4 5 6 7 8 9 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 保存的设置组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 其他执行
由ls-l 得到的某文件的权限设置 prw-rw-r-x 第一个符号为文件类型,后面三个一组分别为用户,组,其他人,rwx对应读写执行权限,-说明没有权限
4.6 新文件和目录的所有权 新文件:
新文件的用户ID设置为进程的有效ID
新文件的组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) ; mode: R_OK (读) W_OK (写) X_OK (执行)
access和faccessat在以下两种情况是相同的:
pathname取绝对路径
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 $ 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) ;
fchmodat,对于flag参数,设置为AT_SYMLINK_NOFOLLOW标志时,fchmodat不会跟随符号链接
对于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-1 GSU2G27:~/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-1 GSU2G27:~/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允许针对目录设置黏着位。如果对一个目录设置了黏着位,只有对该目录具有写权限,并且满足下列条件之一,才能删除或重命名该目录下的文件
拥有此文件
拥有此目录
是超级用户
例如目录/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个函数基本一样,主要区别也就是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) ;
如果长度大于length,截断,如果小于length,文件长度增加,相当于增加的部分创建了空洞文件
4.14 文件系统 本小节简单讲了下,UNIX系统的经典文件系统格式,重点在于i节点和数据块,数据块由目录块和数据块组成,由此可以看出文件真正位置与目录无关,目录的每个目录项储存的是文件的i节点编号和文件名,通过i节点编号来找到实际的数据块,目录块会根据类型字段判断目录项里是否还是一个目录,对于一个没有包含其他目录的叶目录的链接计数总是2(理解:每个目录中都存在一个. 目录,指向当前目录,然后又有创建这个也目录的目录也能指向当前目录,所以链接计数为2),这里的一点疑惑,这个叶目录,在创建它目录的目录项中的i 节点编号(貌似是指向它自己的实际目录块,所以因为没创建文件所以只有. 和 ..目录项)
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) ;
两函数差不多,差别仍然是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) ;
这两个函数删除目录项,并将链接计数-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) ;
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) ;
一些注意点见p96
4.17 符号链接 引入符号是对一个文件的间接指针,引入符号链接的原因,为了避免硬链接的限制
硬链接通常要求链接和文件位于同一文件系统中
只有超级用户才能创建指向目录的硬链接(在底层文件系统支持的情况下)
不过要注意函数是否能处理符号链接,就像之前讲过的通过flag的参数来设置是否追踪
上图的一个例外是,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) ;
因为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) ;
这两个函数组合了open、read和close的所有功能,一气呵成嗷
4.19 文件的时间 文件的时间类型有三种
st_atime 文件数据的最后访问时间 例如: read
st_mtime 文件数据的最后修改时间 例如:write
st_ctime i节点状态的最后更改时间 例如:chmod chown
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) ;
times数组的第一个元素包含访问时间,第二个元素包含修改时间
时间戳按以下方式指定:
如果times参数是空指针,访问和修改时间设置为当前时间
如果非空,任一数组元素的tv_nsec字段的值为UTIME_NOW,相应的时间戳设置为当前时间,忽略tv_sec
如果非空,任意数组元素的值为UTIME_OMIT,相应时间戳保持不变,忽略tv_sec
如果非空,且tv_nsec字段既不是UTIME_NOW,也不是UTIME_OMIT,按相应的设置
执行这些函数所要求的优先权取决于times参数的值
如果是空指针,或者任一tv_nsec字段设为UTIME_NOW,则进程的有效用户ID必须等于该文件的所有者ID;进程对该文件必须具有写权限,或者超级用户进程
如果非空,且既不是UTIME_NOW也不是UTIME_OMIT,进程的有效用户ID必须等于文件所有者ID,或者是超级用户进程,只有写权限是不够的
如果非空,两个字段的值都为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; }
示例: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) ;
注意目录mode至少需要拥有执行权限
1 2 3 #include <unitstd.h> int rmdir (const char *pathname) ;
如果有进程打开目录,不会立马删除,但也不允许创建新文件,
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) ; struct dirent * readdir (DIR *dp); void rewinddir (DIR* dp) ;int closedir (DIR* dp) ; long telldir (DIR* dp) ; void seekdir (DIR* dp, long loc) ;
4.23 函数chdir、fchdir和getcwd 进程调用chdir,fchdir可以修改当前工作目录
1 2 3 4 #include <unistd.h> int chdir (const char *pathname) ;int fchdir (int fd) ;
从当前目录,用..找到上一级目录,,逐层上移,得到当前工作目录的完整绝对路径
1 2 3 #include <unitstd.h> char * getcwd (char *buf, size_t size) ;
4.24 设备特殊文件 st_dev和st_rdev ,在18.9节,编写ttyname函数时,需要使用这两个字段,相关规则:
每个文件系统所在的存储设备都由其主、次设备号表示。设备号所用的数据类型是基本系统数据类型dev_t。主设备号标识设备驱动程序,有时编码为与其通信的外设板;次设备号标识特定的子设备。如4-13图,一个磁盘的驱动器经常包含若干个文件系统。在同一磁盘驱动器上的各文件通常具有相同的主设备号,但是次设备号却不同
我们通常可以使用两个宏:major和minor来访问主、次设备号,大多数实现都定义这两个宏。这就意味着我们无需关心这两个数是如何存放在dev_t对象中的
系统中与每个文件名关联的st_dev值是文件系统的设备号,该文件系统包含了这一文件名以及与其对应的i节点
只有字符特殊文件和块特殊文件才有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
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) ;
根据mode参数的不同值,执行不同工作:
若mode参数为负值,fwide试图指定流是字节定向
若为正,试图指定宽定向
若为0,不试图设置,但返回标识该流定向的值
5.3 标准输入、标准输出和标准错误 每一个进程预定义了3个流,并且自动地被使用,引用的文件就是之前提到过的STDIN_FILENO、STDOUT_FILENO、STDERR_FILLNO。这3个标准I/O流通过预定义文件指针来引用
5.4 缓冲 目的和之前3.9节差不多,尽可能减少read和write的调用次数。标准I/O库的缓冲是自动地进行缓冲管理,避免用户需要考虑怎么设置
标准I/O提供了以下3种类型的缓冲:
全缓冲。在这种情况下,填满标准I/O缓冲区后才进行实际I/O操作,通常在一个流执行第一次I/O操作时,相关函数调用malloc获取所需的缓冲区
术语冲洗(flush)说明标准I/O缓冲区的写操作,注意在UNIX环境中,flush可能有两种意思,标准I/O库方面意味着将缓冲区内容写入磁盘;在终端驱动程序表示丢弃已存储在缓冲区中的数据
行缓冲。在输入和输出中遇到换行符时,标准I/O库执行I/O操作,当流涉及一个终端(标准输入和标准输出)时,通常使用行缓冲
行缓冲的两个限制:a.行缓冲区的长度是固定的,只要填满了,不管有米有换行符,也进行I/O操作 b.标准I/O库要求从一个不带缓冲的流或一个行缓冲的流中获取数据时,也会立刻flush
不带缓冲。标准I/O库不对字符进行缓冲储存,例如,若用标准I/O函数fputs写15个字符到不带缓冲的流,我们期望这15个字符能立刻输出,很可能使用3.8节的write函数将其写到相关文件
通常标准错误流不带缓冲,使得报错信息立刻输出。ISO C要求下列缓冲特征:
当且仅当标准输入和标准输出并不指向交互式设备时,它们才是全缓冲的
标准错误绝对不会是全缓冲
很多系统默认下列类型缓冲:
标准错误是不带缓冲的
若是指向终端设备的流,则是行缓冲,否则是全缓冲
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) ;
setbuf用来简单设定,全缓冲还是不带缓冲,buf的大小由常量BUFSIZ决定,关闭传NULL,如果与终端设备相关,则可能将其设置为行缓冲。
setvbuf用来精确设定,借用mode参数实现: 1._IOFBF(全缓冲) 2. _IOLBF(行缓冲) 3. _IONBF(不带缓冲)
1 2 3 #include <stdio.h> int fflush (FILE *fp) ;
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) ;
三函数的区别:
fopen打开pathname的一个指定文件,type为打开方式
freopen在一个指定的流上打开一个指定的文件,若流已打开,则先关闭流。若该流已定向,清除其定向,此函数一般用于将一个指定文件打开为预定义的流:标准输入,标准输出或标准错误
fdopen根据文件描述符打开一个流,通常用于创建管道和网络通信通道函数返回的描述符,因为这种特殊类型不能用标准I/O函数fopen打开
字符b在UNIX环境下无意义,因为UNIX内核不区分文本文件和二进制文件。对于fdopen,type的参数稍有区别,因为该描述符已经被打开了,区别主要在于截断和创建都不会起作用,因为已经打开,文件已经存在了
当以读和写类型打开一个文件时,具有以下限制: (不太理解什么意思?)
如果中间没有fflush, fseek, fsetpos或rewind,则在输入之后不能直接根输入
如果中间没有fseek, fsetpos或rewind,或者输入操作没有到达文件尾端,输入操作后不能跟输出
1 2 3 4 #include <stdio.h> int fclose (FILE *fp) ;
5.6 读和写流 对于打开的流,有三种非格式化I/O:
每次一个字符的I/O 。一次读写一个字符,如果流带缓冲,标准I/O处理所有缓冲
每次一行的I/O。使用fgets和fputs,每行都以一个换行符终止。
直接I/O。fread和fwrite函数支持这种类型的I/O,每次I/O读取某种数量的对象,比如从二进制文件中每次读写一个结构
输入函数:
1 2 3 4 5 int getc (FILE *fp) ;int fgetc (FILE *fp) ;int getchar (void ) ;
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) ; void clearerr (FILE *fp) ;
大多数实现,流会在FILE对象中维护两个标志:
出错标志
文件结束标志
调用clearerr可以清除这两个标志
1 2 3 int ungetc (int c, FILE *fp) ;
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) ;
与前面输入的区别类似
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) ;
gets从标准输入读,fgets从指定流读,fgets需要指定长度,且对于fgets必须指定缓冲区长度,且每次读的字符数不超过n-1,否则读的是不完整的行,最后一个字符以NULL字节结尾。gets函数不推荐使用,不能指定缓冲区长度,存在缓冲区溢出问题,另一个区别gets不将换行符存入缓冲区
1 2 3 4 int fputs (const char *restrict str, FILE *restrict fp) ;int puts (const char *str) ;
区别puts会在每次输出后添加一个换行符?
5.8 标准I/O的效率 对同一文件,采取不同的I/O函数,结论是,在系统cpu上占用的时间基本一样,时间差别主要在用户cpu上,由于标准I/O中读的时候存在的循环比read长很多(搞不懂这个循环是干啥的,一亿次的循环就离谱),差别主要也是由于这个导致的
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 float data[10 ];if (fwrite (&data[2 ], sizeof (float ), 4 , fp) != 4 ) err_sys ("fwrite error" ); struct { short count; long total; char name[NAMESIZE]; } item; if (fwrite (&item, sizeof (item), 1 , fp) != 1 ) err_sys ("fwrite error" );
二进制I/O的问题,只能用于同一系统上已写的数据,另一个系统可能由于以下原因不能正常工作:
在一个结构中,同一成员的偏移量可能随编译程序和系统的不同而不同
用来存储多字节整数和浮点值的二进制格式在不同系统结构下也可能不同
5.10 定位流 三组不同的,区别不大主要在于偏移量的数据类型的不同
1 2 3 4 5 6 #include <stdio.h> long ftell (FILE *fp) ; int fseek (FILE *fp, long offset, int whence) ; 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) ; int fseeko (FILE *fp, off_t offset, int whence) ; int fgetpos (FILE *restrict fp, fpos_t *restirct pos) ;int fsetpos (FILE *fp, const fpos_t *pos) ;
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, ...) ; 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
1 2 3 4 5 6 7 8 9 10 11 #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, ...) ;
和前面类似: %[*] [fldwidth] [m] [lenmodifier] convtype
用到的时候再细看,P130页
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 ) ;
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 ) ; int mkstemp (char *template ) ;
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" ; char * bad_template = "/tmp/dirXXXXXX" ; 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) ;
与标准I/O流有一定的区别:
无论何时以追加写方式打开内存流,当前文件位置设为缓冲区的第一个null字节,如果缓冲区中不存在null字节,则当前位置就设为缓冲区结尾的后一个字节。当流不是以追加写方式打开,当前位置设置为缓冲区的开始位置。
如果buf参数是一个NULL指针,读写都没有意义,因为没有办法找到缓冲区的位置,就无法读取已写入的内容
任何时候需要增加流缓冲区中的数据量以及调用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) ;
与fmemopen的区别:
创建的流只能写打开
不能指定自己的缓冲区,但可以分别通过bufp和sizep参数访问地址和大小
关闭流后需自行释放缓冲区
对流添加字节会增加缓冲区大小
用法感觉会想到stringstream是不是类似实现的
5.15 标准I/O的替代软件 p140, 提了一些替代软件,有需要再看吧,暂时应该很难用到这么深入的I/O操作
5.16 小结 本章就深入的介绍了I/O库的细节,缓冲技术的细节
第7章 进程环境 7.1 引言 下一章开始进程控制,这一章介绍一些前置知识,以及程序如何运行的
7.2 main函数 1 2 int main (int argc, char * argv[]) ;
内核执行C程序时(使用一个exec函数),在调用main前先调用一个特殊的启动例程。可执行程序文件将此启动例程指定为程序的起始地址(创建虚拟内存?)——-这是由连接编辑器设置的,而连接编辑器则由C编译器调用。启动例程从内核取得命令行参数和环境变量值,然后按上述方式调用main函数做好安排
7.3 进程终止 有8种方式使进程终止,其中5种为正常终止:
从main返回
调用exit
调用_exit或 _Exit
最后一个线程从其启动例程返回
从最后一个线程调用pthread_exit
异常终止有3种方式:
调用abort
接到一个信号
最后一个线程对取消请求做出响应
退出函数:
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 )) ;
ISOC规定一个进程可以登记至多32个函数,这些函数由exit自动调用,称这些为进程终止函数 ,调用atexit函数来登记这些函数
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 #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 2 3 HOME=/home/sar\0 PATH=:/bin:/bash\0
7.6 C程序的存储空间布局 正文段:
CPU执行的机器指令部分。通常,正文段是可共享的,且只读,防止程序意外修改
初始化数据段:
包含了程序种需明确地赋初值的变量。例如 int maxcount = 99(任何函数之外的声明);
未初始化数据段:
通常被称为bss段,例如 long sum[1000];程序开始执行前,将此段中的数据初始化为0或空指针
栈:
自动变量,以及每次函数调用时所需保存的信息存放在此段。每次函数调用时,其返回地址以及调用者的环境信息。最近被调用的函数在栈上为其自动和临时变量分配存储空间
堆:
通常在堆中进行动态存储分配,一般位于栈和未初始化数据段之间
这个图只是一个比较简单的,实际上还存在一些段,动态库那些,视频笔记部分的虚拟内存图比较完整
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) ; void free (void * ptr) ;
基本就是熟悉的那一套
替代的存储空间分配程序:
libmalloc
vmalloc
quick_fit
jemalloc
TCMalloc
alloca
以后学项目的时候遇到再看吧,常规的目前完全够用了
7.9 环境变量 1 2 3 #include <stdlib.h> char * getenv (const char * name) ;
SingleUnixSpecification定义的环境变量:
设置环境变量的三个函数: (只能影响当前进程及其生成和调用的子进程,不能影响父进程的环境)
1 2 3 4 5 6 #include <stdlib.h> int putenv (char * str) ; int setenv (const char * name, const char * value, int rewrite) ;int unsetenv (const char * name) ;
3函数的操作如下:
putenv取形式为name=value的字符串,将其放入环境表中,如果name已经存在,删除其原来的定义
setenv将name设置为value。如果name已经存在,那么(a) 若rewrite非0,则首先删除其现有的定义;(b) 若rewrite为0,则不删除现有定义,不设置新的value也不出错
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 ) { .... }
程序在运行的时候会在栈里面不断创建栈帧
如果在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) ; #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 ) 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) { 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) ; struct rlimit { rlim_t rlim_cur; rlim_t rlim_max; }
进程的资源线程一般由0进程创建,然后由后续进程继承
更改资源限制时,必须遵循下列3条规则:
任何一个进程都可将一个软限制值更改为小于或等于其硬限制值
任何一个进程都可降低其硬限制值,但它必须大于或等于其软限制值,且这种降低对于普通进程是不可逆的
只有超级用户进程才可以提高硬限制值
然后就是一些具体的参数取值,以及代码示例
参数如下:
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 ) ; pid_t getppid (void ) ; uid_t getuid (void ) ; uid_t geteuid (void ) ; gid_t getgid (void ) ; gid_t getdgid (void ) ;
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 ) ; #include <unistd.h> int globvar = 6 ; char buf[] = "a write to stdout\n" ;int main () { int var; pid_t pid; var = 88 ; if (write (STDOUT_FILENO, buf, sizeof (buf) -1 ) != sizeof (buf)-1 ) err_sys ("write error" ); printf ("before fork\n" ); if ((pid = fork()) < 0 ) { err_sys ("fork error" ); } else if (pid == 0 ) { globvar++; var++; } else { sleep (2 ); } 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函数
fork之后处理文件描述符表有以下两种常见的情况:
父进程等待子进程完成。
父进程和子进程各自执行不同的程序段。各自关闭它们不需要的文件描述符
除打开文件之外,其他子进程从父进程继承的属性:
实际用户ID,实际组ID,有效用户ID,有效组ID
附属组ID
进程组ID
会话ID
控制终端
设置用户ID标志的和设置组ID标志
当前工作目录
根目录
文件模式创建屏蔽字
信号屏蔽和安排
对任一打开文件描述符的执行时关闭标志
环境
连接的共享存储段
存储映像
资源限制
父子进程的区别如下:
fork的返回值不同
进程ID不同
子进程的tms_utime,tms_stime,tms_cutime和tms_ustime的值设置为0
子进程不继承父进程设置的文件锁
子进程的未处理闹钟被清除
子进程的未处理信号集被设置为空集
对于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 #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种正常终止方式具体如下:
在main种调用return函数,相当于exit
调用exit函数,exit函数具体作用在 7.3 中提过,但对于UNIX系统不完整,因为它不处理文件描述符,多进程以及作业控制
调用_exit或 _Exit函数。其目的是为进程提供一种无需运行终止处理程序或信号处理程序而终止的方法,对标准I/O流是否冲洗,取决于实现。
进程的最后一个线程在其启动例程中执行return语句。
进程的最后一个线程调用pthread_exit函数。
3种异常终止具体如下:
调用abort。它产生SIGABRT信号,这是下一种异常终止的一种特例。
当进程接收到某些信号时。(第10章会较详细地说明信号)
最后一个线程对 取消 请求作出相应。
不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应的进程关闭所有打开描述符,释放它所使用的存储器。
这里也提及了以下僵尸进程和孤儿进程,僵尸进程产生于,子进程结束了但父进程没有对其进行wait处理,孤儿进程产生于,父进程先于子进程结束,这些子进程会称为init进程的子进程,被其处理
8.6 函数wait和waitpid 当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。可以注册信号捕捉函数来处理它,这种信号的默认处理是忽略。
调用wait或waitpid时可能发生的事情:
如果其所有子进程都还在运行,则阻塞
如果一个子进程已终止,正等待父进程获取其终止状态,则取得该进程的终止状态立即返回
如果它没有任何子进程,则立即返回出错
1 2 3 4 #include <sys/wait.h> pid_t wait (int * statloc) ;pid_t waitpid (pid_t pid, int * statloc, int options) ;
两函数区别:
在一个子进程终止前,wait使其调用者阻塞吗,而waitpid有一选项,可使得调用者不阻塞
waitpid并不等待在其调用之后的第一个终止子进程,它有若干选项,可以控制它等待的进程
statloc传出终止状态,配合特定的宏使用: (上面视频笔记部分的wati就有)
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 ) exit (7 ); if (wait (&status) != pid) err_sys ("wait error" ); pr_exit (status); if ((pid = fork()) < 0 ) err_sys ("fork error" ); else if (pid == 0 ) abort (); if (wait (&status) != pid) err_sys ("wait error" ); pr_exit (status); if ((pid = fork()) < 0 ) err_sys ("fork error" ); else if (pid == 0 ) status /= 0 ; if (wait (&status) != pid) err_sys ("wait error" ); pr_exit (status); exit (0 ); }
waitpid的参数作用如下:
pid == -1 ,等待任一子进程,此时waitpid与wait等效
pid > 0 ,等待进程ID与pid相等的子进程
pid == 0 ,等待组ID等于调用进程组ID的任一子进程
pid < -1 ,等待组ID等于pid绝对值的任一子进程
options参数如下: 此参数或是0,或是如下常量
8.7 函数waitid 此函数类似于waitpid,但更灵活
1 2 3 #include <sys/wait.h> int waitid (idtype_t idtype, id_t id, siginfo_t * infop, int options) ;
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) ;
资源统计信息,包括用户CPU时间总量,系统CPU时间总量,缺页次数,接收到信号的次数等。
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> 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 (); 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); } }
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, ... ) ;int execv (const char * pathname, char * const argv[]) ;int execle (const char * pathname, const char * arg0, ... ) ;int execve (const char * pathname, char * const argv[], char * const envp[]) ;int execlp (const char * filename, const char * arg0, ... ) ;int execvp (const char * filename, char * const argv[]) ;int fexecve (int fd, char * const argv[], char * const envp[]) ;
这些函数的第一个区别,前四个函数以路径名(pathname)作为参数,后两个取文件名(filename)作为参数,最后一个取文件描述符作为参数。当指定文件名为参数时:
如果filename中包含/,则将其视为路径
否则就按PATH环境变量,在它所指定的目录中搜寻可执行文件
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 ); } #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) ;
规则:如果几进程具有超级用户特权,设置的时候对实际用户ID,有效用户ID,保存的设置用户ID,都会被修改;反之,只会修改用户ID
更改3个ID位的另一个方法是执行exec函数的时候,exec会根据 设置用户ID位关闭与否 进行不同更改
设置用户ID位关闭时,实际用户ID不变,有效用户ID不变,保存的用户ID从有效用户ID复制
设置用户ID位打开时,实际用户ID不变,有效用户ID设置为程序文件的用户ID,保存的用户ID从有效复制
函数setreudi和setregid
历史上,BSD支持setreuid函数,其功能是交换实际用户ID和有效用户ID
1 2 3 4 #include <unistd.h> int setreuid (uid_t ruid, uid_t euid) ;int setregid (gid_t rgid, gid_t egid) ;
这种非常规的函数,还是真遇到了再细看
函数seteuid和setegid
类似于setuid和setgid但只会更改有效用户ID和有效组ID
1 2 3 4 #include <unistd.h> int seteuid (uid_t uid) ;int setegid (gid_t gid) ;
可以看出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种返回值:
fork失败或者waitpid返回除EINTR之外的出错,返回-1,并且设置errno
如果exec失败,则其返回值如同shell执行了exit(127)
否则所有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 #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" ); 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 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
还有个注意点,如果一个进程正以特殊的权限(设置用户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 ); } #include "apue.h" int main () { printf ("real uid = %d, effective uid = %d\n" , getuid (), geteuid ()); exit (0 ); } $ 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 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 ;struct acct { char ac_flag; gid_t ac_gid; dev_t ac_tty; time_t ac_btime; comp_t ac_utime; comp_t ac_stime; comp_t ac_etime; comp_t ac_mem; comp_t ac_io; comp_t ac_rw; char ac_comm[8 ]; }
8.15 用户标识 获取运行该程序的用户的登录名
1 2 3 #include <unistd.h> char * getlogin (void ) ;
8.16 进程调度 通过调整友好值,更改进程调度的优先级,友好值越小,优先级越高
1 2 3 #include <unistd.h> int nice (int incr) ;
incr参数被增加到调用进程的友好值上。如果incr太大,系统直接把它降到最大合法值(友好值是有范围的,书上提到的是0 ~ (2*NZERO)-1 ,不过应该取决于实现) ,类似如果incr太小,系统把他提高到最小合法值
1 2 3 #include <sys/resource.h> int getpriority (int which, id_t who) ;
which参数以下三个直之一:
PRIO_PROCESS表示进程
PRIO_PGRP表示进程组
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) ; struct tms { clock_t tms_utime; clock_t tms_stime; clock_t tms_cutime; clock_t tms_cstime; }
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); }
8.18 小结 比较熟悉的是之前视频部分看过的fork wait exec这些函数了,然后本小节,又详细的介绍了这些函数的一些变种,以及system函数的实际实现,进程会计对于进程再一次观察,以及对进程调度优先级的更改
第10章 信号 10.1 引言 信号是软件中断。信号提供了一种处理异步事件的方法。本章先对信号机制进行综述,说明每个信号的一般用法,然后分析早期实现的问题。
10.2 信号概念 产生信号的条件:
当用户按某些终端键,引发终端产生的信号。如ctrl+c通常产生中断信号(SIGINT)
硬件异常产生的信号。如除0操作,非法内存访问
进程调用kill函数可将任意信号发送给另一个进程或进程组。
用户可用kill命令将信号发送给其他进程
当检测到某种软件条件已经发生,并应将其通知有关进程时产生信号。如闹钟产生SIGALRM
信号的三种处理方式:
忽略此信号。
捕捉信号。
执行默认动作。
SIGKILL和SIGSTOP只执行默认动作,提供给系统一个有效的结束进程的方法
P252有各种信号的详细说明
10.3 函数signal 1 2 3 4 5 6 7 #include <signal.h> void (*signal (int signo, void (*func)(int )))(int ); typedef void Sigfunc (int ) ;Sigfunc* signal (int , Sigfunc*) ;
signo参数是信号名。func的参数有三种。
如果指定SIG_IGN,表示忽略此信号
如果指定SIG_DFL,表示执行系统默认动作
如果指定函数地址,当信号发生时,调用该函数,称这种处理为捕捉该信号
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 ) ; 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 $ 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 () ; ... signal (SIGINT, sig_int) ... sig_int () { signal (SIGINT, sig_int); }
问题在于,如果在sig_int中的signal重建捕捉之前,又发生一次中断信号,第二个信号执行默认动作,导致后续的捕捉函数失效。另一个问题在于,没有办法阻止信号发生,你只能忽略它,但没法阻止,现在通过阻塞信号集实现了
10.5 中断的系统调用 早期UNIX的一个特性,如果进程在执行一个低速系统调用而阻塞期间捕捉到了一个信号,则该系统调用就被中断不再继续执行。该系统调用返回出错,其errno设置为EINTR。
低速系统调用:
如果某些类型的文件数据不存在(管道,终端设备,网络设备),则读操作可能使调用者永远阻塞
如果这些数据不能被相同类型文件立即接受,则写操作可能会使调用者永久阻塞
在某种条件发生之前打开某些类型文件,可能发生阻塞(例如打开一个终端设备,需要等待与之连接的调制解调器应答)
pause函数和wait函数
某些ioctl操作
某些进程间通信函数(15章)
10.6 可重入函数 可重入函数,也被称为异步信号安全的。它保证在执行信号处理函数的时候,不会对影响进程的执行。比如如果进程中正在malloc,此时信号产生,去执行信号处理函数,信号处理函数也malloc的话,通常malloc为它分配的存储区维护一个链表,信号处理函数可能更改此链表,由此可能对进程造成破坏
不可重入函数的一些特征:
它们使用静态数据结构
它们调用malloc或free
它们是标准I/O函数,标准I/O库中的很多实现都以不可重入的方式使用全局数据结构
10.7 SIGCLD语义 SIGCLD现在貌似已经没了,了解一下就行
SIGCLD的早期处理方式:
如果进程明确地将该信号的配置设置为SIG_IGN,则调用进程的子进程将不产生僵尸进程。注意,这与默认动作(SIG_DFL)”忽略”不同。子进程终止时,将其状态丢弃。如果调用进程随后调用一个wait函数,那么它将阻塞知道所有子进程都终止,然后wait会返回-1,并将其errno设置为ECHILD。(此信号的默认配置是忽略,但这不会使上述语义起作用,必须将其配置明确指定为SIG_IGN)
如果将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) ;
kill的pid参数如下:
pid > 0 ,将该信号发送给进程ID为pid的进程
pid == 0 ,将信号发送给与发送进程属于同一进程组的所有进程,而且发送进程具有权限向这些进程发送信号
pid < 0 ,将信号发送给进程组ID等于pid绝对值,而且发送进程具有权限向其发送信号的所有进程
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) ;
函数pause使调用进程挂起直至捕捉到一个信号
1 2 #include <unistd.h> int pause (void ) ;
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) { } unsigned int sleep1 (unsigned int seconds) { if (signal (SIGALRM, sig_alrm) == SIGERR) return (seconds); alarm (seconds); pause (); return (alarm (0 )); }
该实现的3个问题:
如果调用sleep之前,调用者已经设置了闹钟,那么sleep1的第一次alarm会将其清除,可用下列方法更正这一点:检查第一次调用alarm的返回值,若其值小于本次调用alarm的参数值,等待已有闹钟超时就行了,如果大于本次调用的值,在sleep1返回之前,重置此闹钟
程序中修改了对SIGALRM信号的配置,需要在sleep1返回之前,改回原有配置
本程序存在竞争问题,如果pause之前,信号超时,产生,然后已经被处理了,那么pause永远也无法再次收到信号,导致程序永久阻塞
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <setjmp.h> #inlcude <signal.h> #include <unistd.h> static jmp_buf env_alrm; static void sig_alrm (int signo) { longjmp (env_alrm, 1 ); } unsigned int sleep2 (unsigned int seconds) { if (signal (SIGALRM, sig_alrm) == SIGERR) return (seconds); 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; 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) { }
两个问题:
仍然存在竞争条件,但我们知道竞争条件,算是一种概率性问题,一般这种需求的程序,alarm设置的很长,所以竞争导致的问题不会发生
如果系统调用是自动重启动的,则当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) ; int sigismemeber (const sigset_t * set, int signo) ;
函数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> #define SIGBAD(signo) ((signo) <= || (signo) > = NSIG) int sigaddset (sigset_t * set, int signo) { if (SIGBAD (signo)) { errno = EINVAL; return (-1 ); } *set |= 1 << (signo - 1 ); return (0 ); } int sigaddset (sigset_t * set, int signo) { if (SIGBAD (signo)) { errno = EINVAL; return -1 ; } *set &= ~(1 << (signo-1 )); 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) ;
set是传入想要修改的信号屏蔽字,oset是返回之前的信号屏蔽字,how可选的参数如下:
SIG_BLOCK 当前信号屏蔽字和set指定信号集的并集,简单来说,就是添加set中的信号
SIG_UNBLOCK 当前信号屏蔽字和set指定信号集补集的交集,简单来说,就是删除set中的信号
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; 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" ); printf ("\n" ); } errno = errno_save; }
10.13 函数sigpending 返回未决信号集
1 2 3 #include <signal.h> int sigpending (sigset_t * set) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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" ); 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" ); 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) ; struct sigaction { void (*sa_handler)(int ); sigset_t sa_mask; int sa_flags; 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 #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) ; 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:" ); if (sigsetjmp (jmpbuf, 1 )) { pr_mask ("ending main: " ); exit (0 ); } canjmp = 1 ; for (;;) pause (); } static void sig_usr1 (int signo) { time_t starttime; if (canjump == 0 ) return ; pr_mask ("starting sig_usr1: " ); alarm (3 ); starttime = time (NULL ); for (;;) 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则不会恢复信号屏蔽字
10.16 函数sigsuspend 之前视频部分分析过这个情况参考(视频部分时序竞态),sigsuspend作为一个原子操作,先恢复信号屏蔽字,然后执行pause的功能,使进程休眠。
1 2 3 #include <signal.h> int sigsuspend (const sigset_t * sigmask) ;
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: " ); if (signal (SIGINT, sig_int) == SIG_ERR) err_sys ("signal(SIGINT) error" ); sigemptyset (&waitmask); sigaddset (&waitmask, SIGUSR1); sigemptyset (&newmask); sigaddset (&newmask, SIGINT); if (isgprocmask (SIG_BLOCK, &newmask, &oldmask) < 0 ) err_sys ("SIG_BLOCK error" ); pr_mask ("in critical region: " ); 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 #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 #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> void abort () { sigset_t mask; struct sigaction action; 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 ); sigfillset (&mask); sigdelset (&mask, SIGABRT); sigprocmask (SIG_SETMASK, &mask, NULL ); kill (getpid (), SIGABRE); fflush (NULL ); action.sa_handler = SIG_DFL; sigaction (SIGABRT, &action, NULL ); sigprocmask (SIG_ABRT, &mask, NULL ); kill (getpid (), SIGABRT); exit (1 ); }
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 $ ./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 #include <sys/wait.h> #include <errno.h> #inclued <signal.h> #include <unistd.h> int system (const char * cmdstring) { pid_t pid; int status; struct sigaction ignore, saveintr, savequit; sigset_t chldmask, savemask; if (cmdstring == NULL ) return 1 ; ignore.sa_handler = SIG_IGN; 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); sigaddset (&chldmask, SIGCHLD); if (sigprocmask (SIG_BLOCK, &chldmask, &savemask) < 0 ) return -1 ; if ((pid = fork()) < 0 ) { status = -1 ; } else if (pid == 0 ) { 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 ; break ; } } 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; }
与之前实现的区别:
当我们键入中断符或退出符时,不向调用进程发送信号
当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) ;
此函数使得进程被挂起知道满足下面两个条件之一:
已经过了seconds所指定的墙上时钟时间
调用进程捕捉到一个信号并从信号处理程序返回
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 #include "apue.h" static void sig_alrm (int signo) { } unsigned int sleep (unsigned int seconds) { struct sigaction newact, oldact; sigset_t newmask, oldmask, suspmask; unsigned int unslept; newact.sa_handler = sig_alrm; sigemptyset (&newact.sa_mask); newact.sa_flag = 00 ; sigaction (SIGALRM, &newmask, &oldmask); alrarm (seconds); suspmask = oldmask; sigdelset (&suspmask, SIGALRM); sigsuspend (&suspmask); unslept = alarm (0 ); sigaction (SIGALRM, &oldact, NULL ); sigprocmask (SIG_SETMASK, &oldmask, NULL ); return (unslept); }
1 2 3 #include <time.h> int nanosleep (const struct timespec* reqtp, struct timespec* remtp) ;
与sleep类似,但提供了纳秒级的精度
1 2 3 #include <time.h> int clock_nanosleep (clockid_t clock_id, int flags, const struct timespec* reqtp, struct timespec* remtp) ;
总之也类似,用到再看 P300
10.20 函数sigqueue 使用信号排队必须做以下几件事:
使用sigaction函数安装信号处理程序时指定SA_SIGINFO标志,如果没有给出这个标志,信号会延迟,但信号是否进入队列取决于具体实现
在sigaction结构的sa_sigaction成员中提供信号处理程序。实现可能允许用户使用sa_handler字段,但不能获取sigqueue发送出来的额外信息
使用sigqueue函数发送信号
1 2 #include <signal.h> int sigqueue (pid_t pid, int signo, const union sigval value) ;
遇到再看吧,这里讲的也很简洁,就大概提了一下
10.21 作业控制信号 POSIX.1认为以下6个信号与作业控制有关
SIGCHLD 子进程已停止或终止
SIGCONT 如果进程已停止,则使其继续运行
SIGSTOP 停止信号(不能被捕捉或忽略)
SIGTSTP 交互式停止信号
SIGTTIN 后台进程组成员读控制终端
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) { }
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 线程概念 线程的好处:
简化处理异步事件的代码。就是用起来更简单吧
多个进程必须使用操作系统提供的复杂机制才能实现内存和文件描述符的共享。
改善响应时间,提高程序的吞吐量
每个线程都包含表示执行环境所必需的信息,其中包括进程中的标识线程的ID,一组寄存器值,栈,调度优先级和策略,信号屏蔽字,errno变量以及线程私有数据
11.3 线程标识 线程ID用pthread_t数据类型来表示,由于实现可能是一个结构体,所以对于线程ID的比较,要用以下函数
1 2 3 #include <pthread.h> int pthread_equal (pthread_t tid1, pthread_t tid2) ;
调用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) ;
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种方式退出:
线程可以简单地从启动例程种返回,返回值是线程的退出码
线程可以被同一进程中的其他线程取消
线程调用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) ;
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) ;
线程可以安排它退出时需要调用的函数,这与进程在退出时可以用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参数:
调用pthread_exit时
响应取消请求时
用非零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_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) ;
11.6 线程同步 线程同步的需求出现在这样一个背景,多个线程对一个变量进行读写,因为写的操作需要两个存储器周期,如果在这两个存储器周期之间,另一个线程又进行操作,那么就会出现竞争导致的问题,为了让线程同步,引入了后续的概念,用于实现线程同步
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) ;
对互斥量加锁,调用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) ;
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 #include <stdlib.h> #include <pthread.h> struct foo { int f_count; pthread_mutex_t f_lock; int f_id; }; 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 ; } } 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 ) { 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 #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; }; 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); 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); 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) ; #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) ;
同样用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) ;
部分实习可能对读锁共享模式下的读写锁次数进行限制,注意这方面的错误
1 2 3 4 5 #include <pthread.h> int pthread_rwlock_tryrdlock (pthread_rwlock_t * rwlock) ;int pthread_rwlock_trywrlock (pthread_rwlock_t * rwlock) ;
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 #include "apue.h" #include <pthread.h> struct job { struct job * j_next; struct job * j_prev; pthread_t j_id; }; struct queue { struct job * q_head; struct job * q_tail; pthread_rwlock_t q_lock; }; 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); return 0 ; } 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); } 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); } 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); } 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) ;
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) ;
同样也可以用常量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) ;
1 2 3 4 #include <pthread.h> int pthread_cond_signal (pthread_cond_t * cond) ;int pthread_cond_broadcast (pthread_cond_t * cond) ;
注意在改变条件状态以后,再给线程发信号
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; }; 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); } } 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) ;
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) ;
重复加锁和对未加锁的自旋锁解锁都是未定义行为
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) ;
count参数指定,在允许所有线程继续允许之前,必须到达屏障的线程数目。可以使用pthread_barrier_wait函数来表明,线程已完成工作,准备等待所有其他线程赶上来
1 2 3 #include <pthread.h> int pthread_barrier_wait (pthread_barrier_t * barrier) ;
对于任意一个线程,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 #define NUMNUM 8000000L #define THUM (NUMNUM/NTHR) 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 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 ; } void * thr_fn (void * arg) { long idx = (long )arg; heapsort (&nums[idx], THUM, sizeof (long ), complong); pthread_barrier_wait (&b); return ((void *)0 ); } 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; srandom (1 ); for (i = 0 ; i< NUMNUM; i++) num[i] = random (); 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 ); 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 #include <pthread.h> int pthread_attr_init (pthread_attr_t * attr) ;int pthread_attr_destroy (pthread_attr_t * attr) ;
分离线程,如果对现有的某个线程的终止状态不关心,可以使用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) ; #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) ;
用途的话,简单来说,一个进程中的虚拟空间中,只有一个栈,被线程共享,如果不够了的话,可以通过设置线程栈属性改变它的位置,通过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) ;
如果希望改变默认栈大小,又不想自己处理线程栈的分配问题,可以用这个属性。设置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) ;
线程属性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) ;
值得注意的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) ;
健壮属性
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) ;
有一说一没看懂是个什么意思,反正两种可能,默认值是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) ;
使用递归锁的情况可能是,在不改变单线程函数接口的情况下,解决并发问题,参考下面两图第一张图,使用递归锁,第二张图不使用
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 *); void * to_arg struct timespec to_wait }; #define SECTONSEC 1000000000 #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 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); 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" ); pthread_mutex_lock (&mutex); if (condition) { 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) ; #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) ;
与其他同步属性一样,条件变量支持进程控制属性。它控制着条件变量是可以被单进程的多个线程使用,还是可以被多进程的线程使用,
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) ;
时钟属性控制计算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) ;
4.屏障属性 同样的初始化和反初始化函数
1 2 3 4 #include <pthread.h> int pthread_barrierattr_init (pthread_barrierattr_t * attr) ;int pthread_barrierattr_destroy (pthread_barrierattr_t * attr) ;
屏障也只有进程共享属性,它控制着屏障是可以被多进程的线程使用,还是只能被初始化屏障内的进程的多线程使用
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) ;
可选值还是那两个 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) ; void flockfile (FILE* fp) ;void funlockfile (FILE* fp) ;int getchar_unlocked (void ) ;int getc_unlocked (FILE* fp) ; int putchar_unlocked (int c) ;int puc_unlocked (int c, FILE* fp) ;
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 <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 ; } #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 *)) ;
对所有的线程可以通过调用pthread_key_delete取消键与线程特定数据之间的关联,但调用它并不会激活与键关联的析构函数。
1 2 3 #include <pthread.h> int pthread_key_delete (pthread_key_t key) ;
需要确保分配的键并不会由于初始化阶段的竞争而发生变动,入下面函数可能会导致两个线程都掉用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 )) ;
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) ; int pthread_setspecific (pthread_key_t key, const void * value) ; #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 #include <pthread.h> int pthread_setcancelstate (int state, int * oldstate) ;
调用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) ;
参数类型: 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) ;
用法基本和sigprocmask一样,但只对当前线程生效
1 2 3 4 #include <signal.h> int sigwait (const sigset_t * restrict set, int * restrict signop) ;
把信号发送给线程,pthread_kill
1 2 3 #included <signal.h> int pthread_kill (pthread_t thread, int signo) ;
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; 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); quitflag = 0 ; if (sigprocmask (SIG_SETMASK, &oldmask, NULL ) < 0 ) err_sys (err, "error" ); exit (0 ); }
12.9 线程和fork 12.10 线程和I/O 12.11 小结