APUE
本文最后更新于:2021年10月25日 下午
0.tips
输入输出问题
在linux系统下使用printf发现有时没有立即输出,有时又能立即输出
原因是printf使用行缓冲,没有刷新缓冲区,故没有输出。
缓冲区刷新的条件:
1.进程结束。
2.遇到\n。
3.缓冲区满。
4.手动刷新缓冲区fflush(stdout)。
一些有用的函数
<stdlib.h>
atoi(str)
用法:将字符串里的数字字符转化为整形数。返回整形值。
注意:转化时跳过前面的空格字符,直到遇上数字或正负符号才开始做转换,而再遇到非数字或字符串结束时(’/0’)才结束转换,并将结果返回。
用于转化argv[]的参数
小知识
- C/C++规定,一个数如果要指明它采用八进制,必须在它前面加上一个0(数字0),如:123是十进制,但0123则表示采用八进制。这就是八进制数在C、C++中的表达方法。(很多宏的掩码就是八进制与)
1.基础
1.1what is os
1.1.1内核
进程线程管理
进程管理主要为linux,windows只是壳子
内存管理
设备驱动
windows .sys文件 linux .ko文件
文件系统
中断子系统
1.1.2内核/用户态
ring0/ring3 环0内核态,环3用户态
ring1/ring2暂时没用到
系统调用 内核为应用程序提供的接口 用户态->内核态
体系结构
shell sh/bash/csh/ksh
库函数 库函数到系统调用
1.2登录
1.2.1 tty/psedo-tty
TTY是电传打字机Teletypewriter的缩写,在带显示屏的视频终端出现之前,TTY是最流行的终端设备
1.2.2 /etc/password
- 密码可以在etc/password看到
- shadow文件
- 起始目录
- 用户id
- 组id
- shell
1.3 文件
1.3.1 一切皆为文件
- 目录
- 文本文件/二进制文件一视同仁
- 设备文件:字符设备、块设备、网络设备(在/dev下ll看第一位)
- socket linux与windows不同
- 管道、消息队列等
1.3.2 相对路径/绝对路径
/ .. .
1.4 I/O
- 文件描述符fd
- stdin/stdout/stderr 对应0/1/2
1.5 进程
进程与程序的关系
进程id
windows下为4的倍数(复用原因)
linux下用ps命令查看
进程控制
fork 子进程都是由父进程fork出来的(写时复制)
exec fork之后调用,类似windows的winexecute api
waitpid
1.6 出错处理
errno
windows下有geterrornumber
strerror
1.7信号
可以理解为用户层的中断,异步打断执行流
信号的处理
系统默认处理: 忽略/中止进程
为指定信号注册处理函数
是Linux编程重点,也容易踩坑
1.8 时间
日历时间
UTC
time-t
进程时间
clock_t
用户CPU时间
系统CPU时间
2.UNIX标准
2.1 标准
ISO C组成
- 语法、语义
- 标准库
ISO C历史
- ANSI C89
- C99
- restrict
- long long
- 单行注释
- 分散代码与声明
- C11
POSIX标准
- 提升各种应用程序在各种UNIX系统环境间的可移植性
- 只定义接口而非实现
SUS(single UNIX specification)
- POSIX的超集
- 扩展了功能
2.2 实现
- FreeBSD
- Linux
- Mac OS X
- Solaris
2.3限制
编译时限制
ISO C
limits.h 各种最大最小值(宏内定义了,include直接用)
float.h 浮点数相关
stdio.h
POSIX 限制
运行时限制
只有运行时才能拿到,例如系统调用给出
- sysconf
- pathconf/fpathconf
3. 无缓冲I/O(文件IO)
3.1 文件描述符fd
所有打开文件都由fd引用,fd为非负int(出错时为负)
类似windows的HANDLE
STDIN_FILENO STDOUT_FILENO STDERR_FILENO
OPEN_MAX(文件打开的极限值)
- 进程各自维护自己的文件描述符表,文件描述符表记录文件描述符标志,和一个指向文件表项的指针
- 文件表项由内核为每一个打开的文件维护,包括文件状态标志、当前文件偏移、以及指向v结点的指针
- ==不同进程打开同一个文件、同一进程调用多次open同一个文件==并不共享file table entry(文件表项) 因为各自的偏移可能不同,也可以理解为调用一次open打开一个文件表项,而复制fd与之无关
- 但是fork出来的子进程的fd指向同一个文件表项(因为文件描述符表也copy,p397)
- ==不同进程打开同一个文件、同一进程调用多次open同一个文件==并不共享file table entry(文件表项) 因为各自的偏移可能不同,也可以理解为调用一次open打开一个文件表项,而复制fd与之无关
/dev/fd
/prof/self/fd
3.2 API
open/openat/create
oflag 文件状态标志
O_RDONLY O_WRONLY O_RDWR
必须指定且互斥
O_APPEND O_CLOEXEC O_CREAT O_DIRECTORY O_EXCL O_SYNC O_DSYNC O_TRUNC
O_CREAT | O_EXCL 配合测试创建文件的原子性
openat的path可以为相对路径
creat只写创建,想要创建写之后再读必须close之后再open,用open实现:
open(path, O_RDWR | O_CREAT | O_TRUNC, mode)
close
会关闭记录锁
进程终止,内核会自动关闭文件对象
RAII的思想
lseek
- 当前文件偏移量
- 文件空洞
read
- 返回值 < 待读字节数的情景
- eof
- 终端设备/行缓冲
- 管道/FIFO
- 中断
write
- 返回值 < 待写字节数的情景
- 磁盘满
- 超过了给定进程的文件长度限制
pread/pwrite
原子lseek + read/write
dup/dup2
- 新描述符的FD_CLOEXEC总是被清除 (即fd flag)
- dup(fd) = fcntl(fd, F_DUPFD, 0);
- dup2(fd) = close(fd2); fcntl(fd, F_DUPFD, fd2); //且为原子操作
- dup 和 fcntl 的 errno 不同
sync/fsync/fdatasync
- 内核维护高速缓存
- 延迟写
- update守护进程周期性调用sync
fcntl(重点)
#include<fcntl.h>
int fcntl(int fd, int cmd, … );
复制一个已有的描述符
- F_DUPFD/F_DUPFD_CLOEXEC
获取/设置文件描述符标志**(fd flag)**
F_GETFD/F_SETFD
仅有的就是FD_CLOEXEC标志
fcntl(fd, F_SETFD, 1); 默认为0,即不关闭
获取/设置文件状态标志
F_GETFL/F_SETFL
==注意GETFL与GETFD的区别==
获取/设置异步I/O所有权
- F_GETOWN/F_SETOWN
获取/设置记录锁
- F_GETLK/F_SETLK/F_SETLKW
ioctl
设备驱动
某个特定操作的接口
类比于windows的 DeviceControl
应用程序到设备驱动的万用接口
4. 文件和目录
4.1 struct stat
POSIX标准 + XSI扩展定义的字段
描述一个特定文件相关的信息
linux下的struct stat
st_mode
文件类型
文件类型 宏 普通文件 regular file S_ISREG() 目录文件 directory file S_ISDIR() 符号链接 symbolic link S_ISLINK() 块特殊文件 block special file S_ISCHR() 字符特殊文件 character special file S_ISBLK() FIFO 命名管道 S_ISFIFO() socket S_ISSOCK() 消息队列 S_TYPEISMQ() 信号量 S_TYPEISEM() 共享内存 S_TYPEISSHM() mode
set-user-id bit S_ISUID
set-group-id bit S_ISGID
进程关联ID:
- 实际用户/组ID
- 有效用户/组ID 附属组ID
文件和目录的权限位
S_ISUID
:执行时设置用户IDS_ISGID
:执行时设置组IDS_ISVTX
:粘着位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.2 文件系统
VFS
- superblock
- incode
- 与文件一一对应,相当于身份证号
- 包含文件的元数据,不包含名称
- 内存中的inode和磁盘中的inode
- 名称->inode->disk block
- dentry
- 文件的逻辑属性
- 一个dentry对应一个inode
- 多个dentry可能对应一个inode(硬链接/软链接)
- file object
4.3 API
状态相关
stat / fstat / statat / lstat
ls - l命令
注意是否跟踪符号链接
权限相关
access / faccessat
探测文件是否存在
以实际用户ID和实际组ID测试访问能力
faccessat提供了 AT_EACCESS 和 AT_SYMLINK_NOFOLLOW
umask
创建新目录或文件时,屏蔽字中置1的权限都会被关闭
区分umask命令
chmod / fchmod / fchmodat
- S_ISUID / S_ISGID
- S_IRWXU / S_IRWXG / S_IRWXO
- S_ISVTX(粘着位在linux无效)
chown / fchown / fchownat / lchown
chown命令
注意符号链接的处理
变更相关
truncate / ftruncate
截断/扩充
link / linkat
unlink / unlinkat
用于确保临时文件被删除
标准库的remove
rename / renameat
- oldname 非目录
- newname存在
- newname不存在
- oldname 为目录
- newname 存在
- newname 不存在
- 符号链接
- . 和 .. 不允许
- oldname 非目录
symlink / symlinkat
readlink / readlinkat
open的局限:不能打开link本身
mkdir / mkdirat
rmdir
控制相关
opendir / fopendir
readdir / rewinddir / closedir / telldir / seekdir
类比文件/标准I/O的接口
chdir / fchdir
getcwd
时间相关
- futimens
- utimensat
- utimes
5.标准I/O
即ISO C标准I/O
5.1 流 (stream)
无缓冲I/O围绕fd展开
有缓冲I/O围绕stream展开
流的定向(stream’s orientation)
单字节 字节定向
byte flow ASCII
多字节 宽定向
wide bytes flow 国际字符集
进程预定义流
stdin / stdout /stderr
流缓冲
意义
类型
全缓冲:填满I/O缓冲区后才进行实际I/O操作
行缓冲:
输入输出遇到换行符时执行I/O操作
2. 由于每行缓冲区固定,缓冲区满没有遇到换行符也要执行I/O 3. 任何时候只要通过标准lO库,要求从一个不带缓冲的流或者一个行缓冲的流(从内核请求数据的时机)得到输入数据,那么就会冲洗所有行缓冲输出流。(即要缓冲输入,先冲洗输出缓冲) 4. 指向终端的流通常使用行缓冲
不带缓冲:不进行缓冲
stdin 和 stdout 并不指向交互设备时,才能使全缓冲类型 (通过重定向)
如果指向终端设备,则是行缓冲的,否则是全缓冲的
stderr绝不能时全缓冲的,一般是不带缓冲
表现为buffersize = 1
5.2 FILE对象
- 不同平台的实现不同
- linux
- fd 用于实际I/O
- buffer指针 buffer尺寸 buffer当前字符数
- 出错标志 文件结束标志
5.3 API
fwide
setbuf / setvbuf p118
fflush
fopen / freopen / fdopen
fclose
读写
- getc / fgetc / getchar / ungetc
- putc / fputc / putchar
- fgets / (gets)
- fputs / (puts)
- fread / fwrite
ferror / feof / clearerr
ftell / ftello / fseek / fseeko / rewind
fgetpos / fsetpos
格式化输入输出
- printf / fprintf / dprintf / sprintf / snprintf
- scanf / fscanf / sscanf
fileno
标准I/O到无缓冲I/O的adapter
tmpnam / tmpfile 临时文件
fmemopen 内存流
6. 系统信息
6.1 数据文件
/etc/passwd
pwd.h中定义了struct passwd结构
可以任意由用户读取
是ASCII文件,可以用标准I/O读取,但是效率太低
因此系统提供API接口
/etc/group
- grp.h中定义了struct group结构
/etc/shadow
经单向加密算法处理过的用户口令副本
shadow.h中定义了struct spwd
阴影口令文件 /etc/shadow 不应该由一般用户读取
仅有少数几个程序需要访问加密口令,如login, passwd, 这些程序通常是设定 set-user-ID为root的程序
6.2 API
6.2.1 数据文件
- getpwuid / getpwnam
- getpwent / setpwent / endpwent
- getspnam / getspent / setspent / endspent
- getgrgid / getgenam
- getgrent / setgrent / endgrent
- getgroups / setgroups / initgroups
返回的结构都是一个静态变量,会覆盖前一次的结果,数据文件的API都提供了 get / set / end 组合技
6.2.2 系统信息
uname
struct utsname
gethostname
time
日历时间
clock_gettime
获取指定的时钟类型的时间
- 实时系统时间
- 不带负跳数的实时系统时间
- 调用进程的CPU时间
- 调用线程的CPU时间
clock_getres
时间精度调整
clock_settime
对特定的时钟设置时间(某些需要权限)
gettimeofday (deprecate)
gmtime / localtime
日历时间转换为struct tm 结构
mktime
strftime / strftime_l
格式化时间,打印字符串
strptime
7. 进程环境
7.1概念
1进程终止
从main返回
exit(main(argc, argv))
return 和 exit(0) 并无区别
exit()库函数
_exit / _Exit 系统调用
这两者区别: 不同标准定义 、是否做了清理工作
2 终止处理程序
atexit函数
事先声明,反向调用,类似栈
3 命令行参数
argc argv
envp 环境表
全局变量 environ
- 函数 getenv / putenv
- 一般调用函数获取环境,而非读取environ
进程地址空间
.text / .data / .bss / stack / heap / others (so , debug , systab )
4 跨越函数跳转
- setjmp / longjmp
- 靠返回值val与正常函数调用区分
5 进程资源限制
- getrlimit / setrlimit
7.2 API
- exit / _Exit / _exit
- atexit
- getenv / putenv / setenv / unsetenv / clearenv
- setjmp / longjmp
8. 进程控制
8.1 概念
进程ID
循环复用
特殊的进程ID
swapper:0 内核交换进程
init:1
普通用户进程
超级用户特权
所有孤儿进程的父进程
进程的创建
一次调用两次返回
父进程中返回创建的子进程ID
子进程返回0
getppid ( get parent id) 获取父进程ID
父子进程谁先执行不确定
子进程是父进程的副本
拷贝数据空间、堆、栈
COW (copy on write)写时复制
试图修改时才真正拷贝
fork + exec 从中受益
代码段共享 (只读)
拷贝文件描述符表
继承父进程相关属性
实际用户ID、实际组ID、有效用户ID、有效组ID 附属组ID 进程组ID 会话ID 控制终端 设置用户ID标识和设置组ID标识 当前工作目录 根目录 文件模式创建屏蔽字 信号屏蔽和安排 对任一打开文件描述符的执行时关闭(close-on-exec)标识 环境变量 连接的共享存储段 存储映像 资源限制
父子进程的不同之处
- ID / PID
- 子进程tms_utime/tms_stime/tms_cutime/tms_ustime被清零
- 子进程不继承父进程文件锁
- 子进程未处理闹钟将被清除
- 子进程未处理信号集设置为空集
父子进程的生死交互
子进程先于父进程终止
通过信号SIGCHILD发送退出状态给父进程
父进程可以通过wait / waitpid获取信息
子进程ID / 子进程终止状态 / CPU耗时
未善后的终止子进程为僵死进程(zombie)
父进程先于子进程终止
子进程父亲改为init 进程(pid 1)
init进程会调用wait善后处理终止的子进程(防止全是zombie)
wait / waitpid
如果所有子进程都还在运行,则阻塞
如果一个子进程终止,正等待父进程获取其终止状态,则取得终止状态立即返回(zombie状态也立即返回)
如果没有任何子进程则出错返回
waitpid功能补充
支持异步(需要设置options参数)
可选择性等待某个进程
pid == -1 / 0 / >0 / <-1
更灵活的wait系列函数
exec族函数
更进一步的封装
popen
system
fork -> exec -> waitpid
权限
- 实际用户 / 组ID
- 有效用户 / 组ID
- setuid / setgid
- 进程拥有超级权限,二话不说直接改实际用户/组ID、有效用户/组ID为指定ID
- 如果没有超级权限,但是uid或gid设置为实际用户/组ID或保存的设置用户/组ID, 则只将有效用户/组ID改为指定ID
- 不满足前两条,通通返回错误(ret = -1, error = EPERM)
解释器文件
- 解释型语言源文件起始行标注
进程调度
优先级
nice
0~2*NZERO-1 sysconf获取
越大越低
getpriority setpriority
进程时间
- 时钟时间
- 用户CPU时间
- 系统CPU时间
- times
8.2 API
- getpid getppid getuid geteuid getgid getegid
- fork vfork
- wait waitpid
- waitid
- wait3/wait4
- execl execv execle execve execlp execvp fexecve
- setuid/setgid
- seteuid/setegid
- system
- nice
- getpriority/setpriority
- times
9. 进程关系
9.1 概念
终端
tty1-6
CTRL ALT F1-6
图形终端
ALT F7
pseudo tty(pty) 网络终端
进程组(作业job)
唯一标志ID
getpgrp / getpgid
setpgid
进程只能为自己和它的子进程设置进程组ID
唯一标志ID和组长的PID一致
多个进程的集合,每个进程都有所属的进程组
同一进程组的所有进程接受同一终端的各种信号
孤儿进程组
每个成员的父亲要么在本组,要么在其他会话中
会话
可以有0或一个控制终端(tty / pseudo tty)
建立与控制终端连接的会话首进程被称为控制进程
无控制终端可能为守护进程
一个到多个进程组的集合
一个前台进程
这意味着会话有一个控制终端
接受 ctrl c / ctrl \ 产生的 SIGINT / SIGQUIT 信号
n个后台进程组
新建会话 setsid
该进程变成新会话的会话首进程 (session leader)
此时该 leader 是会话中的唯一进程
这意味这要新建会话 要先 fork 再 setsid 这就保证了进程不是进程组的组长
该进程成为一个新进程组的组长进程
新进程组ID是调用进程的进程ID
也是会话ID
该进程没有控制终端
如果调用 setsid 前有控制终端,则切断联系
作业控制
9.2 API
- getpgrp / getpgid / sepgid
- setsid / getsid
- tcgetpgrp / tcsetpgrp / tcgetsid
10. 信号
10.1 概念
异步事件
- 信号的产生是不定时的,随机的
- 可以简单理解为用户态的中断(软中断)
产生信号的方式
- 用户按键产生(ctrl C)
- 硬件异常产生
- 进程或者用户调用kill
- 但检测到某种软性条件已经发生,通知有关进程
- SIGURG / SIGPIPE / SIGALARM / SIGABRT
进程处理信号的方式
忽略 ignore
- 大多数信号的默认处理方式
- SIGKILL / SIGSTOP 不能被忽略
- 由硬件异常导致的信号最好不要被忽略
- 中断允许嵌套,但是一般终端过程中同一个中断会忽略
捕捉 catch
- 注册一个signal handler
- 信号到来时会打断当前执行流,转而去执行handler
- 不能捕捉SIGKILL / SIGSTOP
执行默认动作
函数 signal / sigaction
子进程继承了父进程处理信号的方式
被中断的系统调用
低速系统调用
可能会使进程永久阻塞的一类
出错返回
errno EINTR
自动重新启动的系统调用
ioctl read/readv write/writev wait/waitpid
可重入函数
异步信号安全
不可重入的情况
static静态变量
global全局变量
调用了不可重入函数
malloc是线程安全的(递归锁),但是维护共享内存,故是不可重入的
可靠信号
未决的 pending
产生信号和送达之间信号屏蔽字 signal mask (signal procmask)
- 进程可以阻塞某种信号递送sigpending
- 保持未决状态
- 直到进程接触阻塞或设置为忽略才送达
- 阻塞期间同一个信号触发多次(是否排队:sigqueue)
- sigsuspend 解除了 使用 sigprocmask 和 pause 组合 的原子性问题
- 进程可以阻塞某种信号递送sigpending
信号集 sigset_t
相关api : sigemptyset / sigfillset / sigaddset / sigdelset / sigismember
与 sigprocmask 相配合
递送信号
kill / raise 库函数
kill命令
定时器
alarm:SIGALARM 默认动作为终止进程
一个进程只能有一个定时器
睡眠
- sleep / nanosleep / clock_nanosleep
进程控制的延申
- 信号做父子进程的同步
- 加入信号处理的system实现
非局部跳出(deprecated)
sigsetjmp / siglongjmp
对比 setjmp / longjmp
handler自动屏蔽某种信号
跳走后无法保存信号屏蔽字
10.2 API
signal
kill / raise
alarm
pause
sigemptyset / sigfillset / sigaddset / sigdelset / sigismember
sigprocmask / sigpending
sigsetjmp / siglongjmp
sigsuspend
信号屏蔽字被设置为sigmask指向的值然后挂起,在捕捉一个信号之后返回恢复原来的sigmask
abort
11. 线程
11.1 进程与线程的概念
- 进程是资源分配的最小单位,线程是程序执行的最小单位(资源调度的最小单位)
- 进程有自己的独立地址空间,线程是共享进程的地址空间
- 线程之间通信更方便,进程则要通过各种IPC机制
- 多进程更加健壮,一个进程异常挂掉不会导致其他进程挂掉
11.2 多线程与多核
- 单核也可以多线程
- 多核多线程可以达成同时run的效果
11.3 pthread标准
POSIX 线程
LinuxThreads的变化(NPTL)
11.4 线程信息
以下不共享
线程ID
pthread_t / pthread_equal / pthread_self
栈
信号屏蔽字
调度优先级
errno变量
线程私有数据
11.5 线程的生与死
线程创建
pthread_create
线程创建不能保证哪个线程会先运行
线程继承调用线程的浮点环境和信号屏蔽字
但是 pending 信号集会被清除
线程终止
导致进程终止
- 任意线程调用 exit / _exit / _Exit
- 发送给线程的信号(默认终止进程)
单一线程终止
线程从启动历程返回
pthread_join / pthread_datch
线程被同一进程的其他线程取消
pthread_cancel
线程调用 pthread_exit
线程清理处理程序
- pthread_cleanup_push / pthread_cleanup_pop
- 清理情况:
- 主动调用 pthread_exit
- 响应 pthread_cancel
- 调用 pthread_cleanup_pop 参数不为0的时候
- 直接 return 并不会执行清理
- 执行顺序与注册顺序相反
11.6 进程与线程原语
进程原语 | 线程原语 | 描述 |
---|---|---|
fork | pthread_create | 创建新的控制流 |
exit | pthread_exit | 从现有的控制流中退出 |
waitpid | pthread_join | 从控制流中得到退出状态 |
atexit | pthread_cancel_push | 注册在退出控制流时调用的函数 |
getpid | pthread_self | 获取控制流的ID |
abort | pthread_cancel | 请求控制流的非正常退出 |
11.7 线程同步、一致性问题
原子操作
互斥锁
POSIX互斥量
struct pthread_mutex_t
初始化/销毁
pthread_mutex_init / pthread_mutex_destory
上锁
pthread_mutex_lock / pthread_mutex_timedlock / pthread_mutex_trylock / pthread_mutex_unlock
解锁
pthread_mutex_unlock
死锁
AB型死锁
解决方法:
- 按序获取锁(程序复杂)
- trylock / timedlock
读写锁(共享互斥锁)
状态
读锁:读请求 pass ,写请求阻塞直到读锁释放(共享锁)
引用计数实现
注意写请求的饥饿情况,通常写请求后的读请求被阻塞(FIFO)
写锁:阻塞任何的加锁请求(互斥锁)
无锁
一次只有一个线程可以占有写锁,可以有多个线程同时占有读锁
适用于读请求>>写请求的情况
POSIX读写锁
初始化 / 销毁
pthread_rwlock_init / pthread_rwlock_destroy
读锁
pthread_rwlock_rdlock / pthread_rwlock_tryrdlock / pthread_rwlock_timedrdlock
写锁
pthread_rwlock_wrlock / pthread_rwlock_trywrlock / pthread_rwlock_timedwrlock
解锁
pthread_rwlock_unlock
条件变量
配合互斥量使用,提供多线程会合的时间点
初始化 / 销毁
pthread_cond_init / pthread_cond_destroy
等待条件变量变为true
pthread_cond_wait / pthread_cond_timedwait
条件变量置信
pthread_cond_signal 唤醒一个 / pthread_cond_broadcast 唤醒所有
自旋锁 spinlock
- 特征:忙等阻塞
- 锁持有时间段,线程不希望被调度
- 用户态基本不使用自旋锁
- 不要调用在持有自旋锁的情况下可能会进入休眠状态的函数
屏障 barrier
协调多个进程并行工作
每个线程等待,直到所有线程共同达到某一点
初始化 / 销毁
pthread_barrier_init / pthread_barrier_destory
等待
pthread_barrier_wait
未满足屏障计数时阻塞 、满足屏障计数时唤醒所有(最后一个线程)
12. 线程控制
12.1 属性
pthread遵循的对于属性的模式
- 每个对象都和自己类型的属性对象相关联(互斥量与互斥量属性相关联,线程与线程属性相关联),表现为 attr 指针,每个属性对象可以代表多个属性。属性对应用不透明,便于提高可移植性,因此需要函数来进行管理
- 每个属性对象有一个初始化函数,它把属性设置为默认值
- 还有一个销毁属性对象的函数,用于释放与属性对象的资源
- 获取各个属性值的函数,返回存储它的内存单元
- 设置属性值的函数,一般来说属性作为参数用指针传递
线程属性
初始化 / 销毁
pthread_attr_init / pthread_attr_destory
线程分离状态属性(分离线程的资源在线程终止时立即收回,无法用 join 等待其终止状态)
pthread_attr_getdetachstate / pthread_attr_setdetachstate
以下不建议用
pthread_attr_getguardsize / pthread_attr_setguardsize
pthread_attr_getstacksize / pthread_attr_setstacksize
pthread_attr_getstack / pthread_attr_setstack
取消状态(取消点)不建议用
PTHREAD_CANCEL_ENABLE / PTHREAD_CANCEL_DISABLE
pthread_setcancelstate
被取消线程在调用点会感知到取消 (pthread_cancel调用方不等待)
默认情况延迟取消
同步属性
互斥量属性 pthread_mutexattr_t
共享属性、健壮属性、类型属性
pthread_mutexattr_init / pthread_mutexattr_destroy
以下不建议使用
进程共享(内核开销大,不属于NPTL)
pthread_mutexattr_getpshared / pthread_mutexattr_setpshared
健壮属性
类型属性
pthread_mutexattr_gettype / pthread_mutexattr_settype
互斥量类型 递归上锁 不占用时解锁 已解锁时解锁 PTHREAD_MUTEX_NORMAL 死锁 未定义 未定义 PTHREAD_MUTEX_ERRORCHECK 返回错误 返回错误 返回错误 PTHREAD_MUTEX_RECURSIVE 允许 返回错误 返回错误 PTHREAD_MUTEX_DEFAULT 未定义 未定义 未定义 递归锁的使用场景
读写锁属性 pthread_rwlockattr_t
- pthread_rwlockattr_init / pthread_rwlockattr_destroy
- pthread_rwlockattr_getpshared / pthread_rwlockattr_setpshared
条件变量属性 pthread_condattr_t
- pthread_condattr_init / pthread_condattr_destroy
- pthread_condattr_getpshared / pthread_condattr_setpshared
屏障属性 pthread_barrierattr_t
- pthread_barrierattr_init / pthread_barrierattr_destroy
- pthread_barrierattr_getpshared / pthread_barrierattr_setpshared
12.2 重入
概念
如果一个函数对多个线程来说时可重入的,就说这个函数是线程安全的。但并不能说明对信号处理程序来说该函数也是可重入的。
如果函数对于异步信号处理程序的重入是安全的,那么可以说函数是异步信号安全的
重入的要求高于线程安全
可重入要求信号安全
一般来说:
如果一个函数的实现使用了全局或者静态变量,且访问未加锁,那么这个函数既不是可重入的,也不是线程安全的。
如果放宽条件,这个函数仍然用到了全局或者静态变量,但是在访问这些变量时,通过加锁来保证互斥访问,那么这个函数就可以变成线程安全的函数。但它此时仍然是不可重入的,因为通常加锁是针对不同线程的访问,对同一线程可能出现问题(发生信号软中断,signal handler中恰巧也执行了该函数)。
非线程安全函数
替代的线程安全函数
12.3 线程私有数据
由于线程共享地址空间,故线程无法阻止另一个线程访问其私有数据
因此需要管理线程特定数据的函数(设计上封装隔离)
pthread_key_create / pthread_key_delete
让不同线程看到同一个键值 pthread_once
pthread_once_t = PTHREAD_ONCE_INIT
1
2
3
4
5
6
7
8
9
10
11
12
13
14void destructor(void *);
pthread_key_t key;
pthread_once_t init_done = PTHREAD_ONCE_INIT;
void
thread_init(Void)
{
err = pthread_key_create(&key, destructor);
}
int
threadfunc(void *arg)
{
pthread_once(&init_done, thread_init);
...
}关联键和私有数据
pthread_getspecific / pthread_setspecific
12.4 线程和信号
线程都有自己的信号屏蔽字
线程的处理是进程中所有线程共享的
信号是传递给单个线程的
- 信号与硬件相关,递送给引起时间的进程
- 其他信号发送给任意一个进程
pthread_sigmask
sigprocmask在多线程环境中行为未定义
用法类似sigprocmask
sigwait等待信号出现
先阻塞等待的信号(在外部)
原子取消信号集阻塞状态
信号递送后返回
返回前恢复阻塞信号集
类比条件变量和互斥量
多个线程等待同一信号,只有一个会被唤醒
pthread_kill
给指定进程发送信号
线程与I/O
lseek read
多线程有问题
pread
lseek read 的原子操作
12.5 API
- pthread_attr_init / pthread_attr_destroy
- pthread_attr_getdetachstate / pthread_attr_setdetachstate
- pthread_key_create
- pthread_mutexattr_init / pthread_mutexattr_destroy
- pthread_mutexattr_gettype / pthread_mutexattr_settype
- pthread_key_create / pthread_key_delete
- pthread_getspecific / pthread_setspecific
- pthread_sigmask
- pthread_sigkill
13. 守护进程
13.1 概念
守护进程的概念
- 内核态守护进程(内核线程)
- eg: 虚拟内存换页kswapd / 脏页面冲刷 flush
- 用户态守护进程
- 由 init 拉起
- setsid 使其一般是会话首进程,同时也是进程组组长、唯一进程
编写守护进程的惯例
umask 设置文件模式创建屏蔽字
通常 umask(0)
父进程 fork 并 exit
为子进程 setsid 创建会话创造条件
setsid
会话首进程 / 进程组组长 / 没有控制终端
当前工作目录改为根目录或者其他位置
chdir(“/“) 防挂在umount
关闭不用的文件描述符
首先 getrlimit 判定最高文件描述符的值,然后用循环全部关闭
打开 /dev/null 使具有文件描述符0、1、2
因为守护进程并不与终端设备相关联,无从显示也无需输入
fd0 = open(“/dev/null”, O_RDWR);
一般还需要处理 SIGHUP 信号
原因:孤儿进程
出错记录
- 内核例程调用log函数
- 用户守护进程调用 syslog 函数
- 本地或其他主机可通过 UDP 514端口传递log
- rsyslog
单例守护进程
文件记录锁
记录锁
惯例
锁通常指定在 /var/run/%name%.pid
内容一般就是pid号
配置文件通常在 /etc/%name%.conf
守护进程一般通过初始化脚本之一启动
/etc/rc* /etc/init.d/* /etc/inittab启动自动重启
一般注册 SIGHUP 处理程序
一方面使为了防止默认动作终止
功能上设置为重新读取配置
13.2 API
- openlog / syslog / closelog / setlog / mask
- vsyslog
14 进阶I/O
14.1 非阻塞I/O
低速系统调用
定义:会引起进程永久阻塞
- 某些文件类型数据不存在,读引起永久阻塞
- 数据不能被相同的文件类型接受,写操作会永久阻塞
- 对加了记录锁的文件读写
- ioctl
- 进程通信函数
open 先天指定 O_NONBLOCK
fcntl 后天设置 O_NONBLOCK
轮询 polling + 非阻塞I/O
类似用户态的自旋锁 浪费cpu时间
多线程 + 阻塞I/O
额外的线程开销、同步开销
14.2 记录锁 recording lock
确保进程单独写文件
进程读或写文件的某个部分时,使用记录锁组织其他进程修改同一文件区
byte range locking
fcntl 记录锁
struct flock
1
2
3
4
5
6
7struct flock{
short l_type;
short l_whence;
off_t l_start;
off_t l_len;
pid_t l_pid;
}l_type : F_GETLK / F_SETLK / F_SETLKW
F_GETLK 检测上锁后上锁不是原子操作
F_SETLKW 检测到死锁后杀死另一个进程获得资源
锁的隐含继承与释放
进程终止时,建立的所有锁全部释放
关联的fd何时关闭,锁都会释放
fork 子进程只能继承文件描述符,不能继承它的锁
文件尾端加锁
建议性锁和强制性锁
14.3 异步I/O
不建议用
14.4 I/O多路转接(multiplexing)
问题提出
阻塞模式下在多个fd上写,一个block会导致后面的pending
polling + 无阻塞可以解决
异步I/O 用信号通知
缺点在于不知道哪个fd ready(不够映射)
I/O多路复用
select
- select / pselect
- pselect 支持 timespec 结构,更精确的时间
- 最多支持fd 有上限
poll
- 通过数组表明关心的条件
- 解决了fd数量的瓶颈
epoll
- linux I/O多路转接的最优机制
- 性能高
- 规避了所有fd的用户态 copy 到内核态的开销
- fd常驻内核
- 内核以红黑树组织
- 内核态只回传ready 部分的fd
- 边沿触发
- 规避了所有fd的用户态 copy 到内核态的开销
14.5 其他
readv / writev
散布读(scatter read) / 聚集写(gather write)
iovec 结构数组
1
2
3
4struct iovec{
void *iov_base;
size_t iov_len
}
降低系统调用的次数,获取性能
readn / writen
apue 对 read / write 的一些容错封装
原因: 管道、FIFO、网络、终端
可能读的字节数小于指定数量
写可能因为内核缓冲区满而失效
mmap / munmap
存储映射I/O
磁盘文件映射到内存空间
直接读写内存就是修改磁盘文件
mprotect
修改映射区权限
msync
立刻同步刷新
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!