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暂时没用到

  • 系统调用 内核为应用程序提供的接口 用户态->内核态

  • 体系结构

    image-20211007104949635

    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(文件打开的极限值)

  • image-20211008102820346

  • image-20211008103035519

    • 进程各自维护自己的文件描述符表,文件描述符表记录文件描述符标志,和一个指向文件表项的指针
    • 文件表项由内核为每一个打开的文件维护,包括文件状态标志、当前文件偏移、以及指向v结点的指针
      • ==不同进程打开同一个文件、同一进程调用多次open同一个文件==并不共享file table entry(文件表项) 因为各自的偏移可能不同,也可以理解为调用一次open打开一个文件表项,而复制fd与之无关
        • 但是fork出来的子进程的fd指向同一个文件表项(因为文件描述符表也copy,p397)
  • /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

  • image-20211008212224736
  • 新描述符的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:

      1. 实际用户/组ID
      2. 有效用户/组ID 附属组ID
    • 文件和目录的权限位

      • S_ISUID:执行时设置用户ID
      • S_ISGID:执行时设置组ID
      • S_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:用户执行

      image-20211010161123024

    • 鉴权流程

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 不存在
    • 符号链接
    • . 和 .. 不允许
  • 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操作

      行缓冲:

      1. 输入输出遇到换行符时执行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

    获取指定的时钟类型的时间

    1. 实时系统时间
    2. 不带负跳数的实时系统时间
    3. 调用进程的CPU时间
    4. 调用线程的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 系统调用

  • 这两者区别: 不同标准定义 、是否做了清理工作

  • image-20211012235649755

2 终止处理程序

  • atexit函数

    事先声明,反向调用,类似栈

3 命令行参数

  • argc argv

  • envp 环境表

    全局变量 environ

    image-20211012235941033

    • 函数 getenv / putenv
    • 一般调用函数获取环境,而非读取environ
  • 进程地址空间

    image-20211013000146968

    .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 从中受益

  • 代码段共享 (只读)

  • 拷贝文件描述符表

    image-20211013001243684

  • 继承父进程相关属性

    实际用户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 概念

终端

image-20211015155106020

  • 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个后台进程组

    • image-20211015154131834

  • 新建会话 setsid

    • 该进程变成新会话的会话首进程 (session leader)

      此时该 leader 是会话中的唯一进程

      这意味这要新建会话 要先 fork 再 setsid 这就保证了进程不是进程组的组长

    • 该进程成为一个新进程组的组长进程

      新进程组ID是调用进程的进程ID

      也是会话ID

    • 该进程没有控制终端

      如果调用 setsid 前有控制终端,则切断联系

作业控制

image-20211015155022811

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
  • 执行默认动作

    image-20211013234629726

  • 函数 signal / sigaction

  • 子进程继承了父进程处理信号的方式

被中断的系统调用

  • 低速系统调用

    可能会使进程永久阻塞的一类

  • 出错返回

    errno EINTR

  • 自动重新启动的系统调用

    ioctl read/readv write/writev wait/waitpid

可重入函数

  • 异步信号安全

    image-20211013235025005

  • 不可重入的情况

    static静态变量

    global全局变量

    调用了不可重入函数

  • malloc是线程安全的(递归锁),但是维护共享内存,故是不可重入的

可靠信号

  • 未决的 pending
    产生信号和送达之间

  • 信号屏蔽字 signal mask (signal procmask)

    • 进程可以阻塞某种信号递送sigpending
      • 保持未决状态
      • 直到进程接触阻塞或设置为忽略才送达
      • 阻塞期间同一个信号触发多次(是否排队:sigqueue)
    • sigsuspend 解除了 使用 sigprocmask 和 pause 组合 的原子性问题
  • 信号集 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型死锁

      解决方法:

      1. 按序获取锁(程序复杂)
      2. 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遵循的对于属性的模式

  1. 每个对象都和自己类型的属性对象相关联(互斥量与互斥量属性相关联,线程与线程属性相关联),表现为 attr 指针,每个属性对象可以代表多个属性。属性对应用不透明,便于提高可移植性,因此需要函数来进行管理
  2. 每个属性对象有一个初始化函数,它把属性设置为默认值
  3. 还有一个销毁属性对象的函数,用于释放与属性对象的资源
  4. 获取各个属性值的函数,返回存储它的内存单元
  5. 设置属性值的函数,一般来说属性作为参数用指针传递

线程属性

  • 初始化 / 销毁

    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 未定义 未定义 未定义
      • 递归锁的使用场景

        image-20211015230742334

读写锁属性 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中恰巧也执行了该函数)。

非线程安全函数

image-20211015231532218

替代的线程安全函数

image-20211015231605762

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
    14
    void 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 使其一般是会话首进程,同时也是进程组组长、唯一进程

编写守护进程的惯例

  1. umask 设置文件模式创建屏蔽字

    通常 umask(0)

  2. 父进程 fork 并 exit

    为子进程 setsid 创建会话创造条件

  3. setsid

会话首进程 / 进程组组长 / 没有控制终端

  1. 当前工作目录改为根目录或者其他位置

    chdir(“/“) 防挂在umount

  2. 关闭不用的文件描述符

    首先 getrlimit 判定最高文件描述符的值,然后用循环全部关闭

  3. 打开 /dev/null 使具有文件描述符0、1、2

    因为守护进程并不与终端设备相关联,无从显示也无需输入

    fd0 = open(“/dev/null”, O_RDWR);

  4. 一般还需要处理 SIGHUP 信号

    原因:孤儿进程

出错记录

  • image-20211015160833072
  • 内核例程调用log函数
  • 用户守护进程调用 syslog 函数
  • 本地或其他主机可通过 UDP 514端口传递log
  • rsyslog

单例守护进程

  • 文件记录锁

    记录锁

  • 惯例

    1. 锁通常指定在 /var/run/%name%.pid

      内容一般就是pid号

    2. 配置文件通常在 /etc/%name%.conf

    3. 守护进程一般通过初始化脚本之一启动

      /etc/rc* /etc/init.d/* /etc/inittab启动自动重启

    4. 一般注册 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
      7
      struct 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 检测到死锁后杀死另一个进程获得资源

  • 锁的隐含继承与释放

    1. 进程终止时,建立的所有锁全部释放

    2. 关联的fd何时关闭,锁都会释放

    3. 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
    • 边沿触发

14.5 其他

readv / writev

  • 散布读(scatter read) / 聚集写(gather write)

    • iovec 结构数组

      1
      2
      3
      4
      struct 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 协议 ,转载请注明出处!