Linux 进程调度命令

进程管理

ps

查看当前进程瞬时快照

top

显示当前正在运行进程(动态更新)

  • 按照使用内存大小排序,可以用于查找内存使用情况

pgrep

按名称、属性查找进程

pidof

根据进程名查找正在运行的进程进程号

kill

终止进程

killall

按名称终止进程

pkill

按名称、属性终止进程

timeout

在指定时间后仍然运行则终止进程

wait

等待指定进程

fuser

显示使用指定文件、socket的进程

pmap

报告进程的内存映射

lsof

列出打开的文件

chkconfig

为系统服务更新、查询运行级别信息

作业

&

放在命令之后,命令后台执行

1
2
3
$ ./pso > pso.file 2>&1 &
# 将`pso`放在后台运行,把终端输出(包括标准错误)
# 重定向的到文件中

nohup

不挂起job,即使shell退出

1
2
3
4
$ nohup ./pso > pso.file 2>&1 &
# 不挂起任务,输出重定向到文件
$ nohup -p PID
# 不挂起某个进程

jobs

列出活动的作业

-l:返回任务编号、进程号

bg

恢复在后台暂停工作的作业

1
2
$ bg %n
# 将编号为`n`的任务转后台运行

fg

将程序、命令放在前台执行

1
2
$ fg %n
# 将编号为`n`的任务转前台运行

setsid

在一个新的会话中运行程序

1
2
3
4
$ setsid ./test.sh &`
# 新会话中非中断执行程序,此时当前shell退出不会终止job
$ (./test.sh &)
# 同`setsid`,用`()`括起,进程在subshell中执行

`disown

1
2
3
$ disown -h %job_id
# *放逐*已经在后台运行的job,
# 则即使当前shell退出,job也不会结束

screen

创建断开模式的虚拟终端

1
2
3
4
5
6
$ screen -dmS screen_test
# 创建断开(守护进程)模式的虚拟终端screen_test
$ screen -list
# 列出虚拟终端
$ screen -r screen_test
# 重新连接screen_test,此时执行的任何命令都能达到nohup

快捷键

  • <c-z>:挂起当前任务
  • <c-c>:结束当前任务

Linux System Call

Kernel

内核:提供硬件抽象层、磁盘及文件系统控制、多任务等功能的系统 软件

  • 内核是操作系统的核心、基础,决定系统的性能、稳定性

    • 内核单独不是完整的操作系统
  • 内核为应用程序提供对硬件访问

    • 应用程序对硬件访受限
      • 内核决定程序何时、对何种硬件操作多长时间
    • 内核提供硬件抽象的方法隐藏对硬件操作的复杂
      • 为应用程序和硬件提供简洁、统一的接口
      • 简化程序设计

内核功能

  • 进程管理:实现了多个进程在CPU上的抽象

    • 负责创建、销毁进程,处理它们和外部输入、输出
    • 处理进程之间通讯
      • 信号
      • 管道
      • 通讯原语
    • 调度器控制进程如何共享CPU
  • 内存管理:内存是主要资源,对其管理策略对性能影响非常重要

    • 为所有进程在有限资源上建立虚拟寻址空间
    • 内核不同部分与内存管理子系统通过函数调用交互,实现 mallocfree等功能
  • 文件管理:Linux很大程度上基于文件系统概念,几乎任何东西 都可以视为是文件

    • 在非结构化硬件上建立了结构化文件系统
    • 支持多个文件系统,即物理介质上的不同数据组织方式
  • 驱动管理

    • 除CPU、内存和极少的硬件实体外,基本设备控制操作都由 特定的、需寻址的设备相关代码(设备驱动)进行
    • 内核中必须嵌入系统中出现每个外设驱动
  • 网络管理

    • 网络必须由系统管理
      • 大部分网络操作不是特定于某个进程:进入系统的报文 是异步事件
      • 系统在进程接手报文前收集、识别、分发,在程序和 网络接口间递送数据报文,根据程序的网络活动控制 程序执行
    • 路由、地址解析也在内核中实现

System Call

系统调用:操作系统提供的实现系统功能的子程序、访问硬件资源 的唯一入口

  • 系统调用是用户空间进程访问内核、硬件设备的唯一手段

    • 用户空间进程不能直接访问内核、调用内核函数
    • 对计算机硬件资源的必须经过操作系统控制
      • 计算机系统硬件资源有限,多个进程都需要访问资源
  • 系统调用与硬件体系结构紧密相关

    • 在用户空间进程和硬件设备之间添加中间层,是二者沟通的 桥梁
    • 是设备驱动程序中定义的函数最终被调用的一种方式

syscall_routine_procedure

系统调用意义

  • 用户程序通过系统调用使用硬件,简化开发、移植性

    • 分离了用户程序和内核的开发
      • 用户程序忽略API具体实现,仅借助其开发应用
      • 内核忽略API被调用,只需关心系统调用API实现
    • 为用户空间提供了统一硬件抽象接口,用户程序可以方便在 具有相同系统调用不同平台之间迁移
  • 系统调用保证了系统稳定和安全

    • 内核可以基于权限和其他规则对需要进行的访问进行 裁决
    • 避免程序不正确的使用硬件设备、窃取其他进程资源、 危害系统安全
  • 保证进程可以正常运行在虚拟寻址空间中

    • 程序可以随意访问硬件、内核而对此没有足够了解, 则难以实现多任务、虚拟内存
    • 无法实现良好的稳定性、安全性

通知内核

  • 大部分情况下,程序通过API而不是直接调用系统调用
  • POSIX标准是Unix世界最流行的API接口规范,其中API和系统 调用之间有直接关系,但也不是一一对应

系统调用表

系统调用表sys_call_table:其中元素是系统调用服务例程的 起始地址

1
2
3
4
5
// arch/x86/entry/syscall_64.c
asmlinkage const sys_call_ptr sys_call_table[__NR_syscall_max+1] ={
[0 ... __NR_syscall_max] = &sys_ni_syscall,
#include <asm/syscalls_64.h>
};
  • ...是GCCDesignated Initializers插件功能,此插件允许 无序初始化元素值
  • sys_call_table是长为__NR_syscall_max+1的数组

  • __NR_syscall_max是给定系统架构所允许的最大系统调用 数目

    1
    2
    // include/generated/asm-offsets.h
    #define __NR_syscall_max 547
    • 此数值必然和arch/x86/entry/syscalls/syscall_64.tbl 中最大系统调用数值相同
  • sys_call_ptr是指向系统调用表指针类型

    1
    typedef void (*sys_call_ptr_t)(void);
  • sys_ni_syscall是返回错误的函数

    1
    2
    3
    asmlinkage long sys_ni_syscall(void){
    return -ENOSYS;
    }
    • 未在<asm/syscalls_64.h>定义系统调用号将会对应此 响应函数,返回ENOSYS专属错误
  • <asm/syscalls_64.h>由脚本 arch/x86/entry/syscalls/syscalltbl.sharch/x86/entry/syscalls/syscall_64.tbl中生成

    1
    2
    3
    4
    5
    6
    7
    // <asm/syscalls_64.h>
    __SYS_COMMON(0, sys_read, sys_read)
    __SYS_COMMON(0, sys_write, sys_write)

    // <arch/x86/entry/syscall_64.c>
    #define __SYSCALL_COMMON(nr, sym, compat) __SYSCALL_64(nr, sym, compat)
    #define __SYSCALL_64(nr, sym, compat) [nr] = sym

系统调用号

1
2
3
4
5
6
7
/* fs/xattr.c */
#define __NR_setxattr 5
__SYSCALL(__NR_setxattr, sys_setxattr)
#define __NR_lsetxattr 6
__SYSYCALL(__NR_lsetxattr, sys_lsetattr)
#define __NR_fsetxattr 7
__SYSYCALL(__NR_fsetxattr, sys_fsetattr)

系统调用号_NR_XXX:系统调用唯一标识

  • 系统调用号一旦分配不能有任何变更,否则编译好的程序会崩溃

    • 系统调用号定义在include/asm/unisted.h
  • 用户空间进程通过系统调用号指明需要执行的系统调用

    • 系统调用号就是系统调用在sys_call_table中的偏移
    • 根据系统调用号在sys_call_table中找到对应表项内容, 即可找到系统调用响应函数sys_NAME的入口地址
  • 所有系统调用陷入内核的方式相同,所以必须把系统调用号一并 传给内核

    • X86机器上,系统调用号通过eax寄存器传递给内核
      • 陷入内核态,用户空间进程已经把系统调用对应系统 调用号放入eax
      • 系统调用一旦运行,就可以从eax中得到数据

陷入指令

  • 系统调用通过陷入指令进入内核态

    • 然后内核根据存储在寄存器中的系统调用号在系统调用表 中找到相应例程函数的入口地址
    • 根据入口地址调用例程函数
  • 陷入指令是特殊指令,且依赖于机器架构,在X86机器中指令为 int 0x80

    • 不应直接使用陷入指令
    • 应实现系统调用库函数,以系统调用号为参数,执行陷入 指令陷入内核态,并执行系统调用例程函数,即 __syscall[N]系列宏

__syscall[N]

_syscall[N]:方便用户程序访问系统调用的一系列宏

1
2
3
4
5
6
// linux/include/asm/unistd.h
__syscall0(type, name)
__syscall1(type, name, type1, args1)
__syscall2(type, name, type1, arg1, type2, arg2)
// 0-6共7个宏
__syscall6(type, name, type1, arg1, type2, arg2, type3, arg3, type4, arg4, type5, arg5, type6, arg6)
1
2
3
4
5
6
7
8
9
10
#define _syscall2(type, name, type1, arg1, type2, arg2) \
type name(type1, arg1, type2, arg2) \
{ \
long _res; \
__asm__ volatile ("int $0x80" \
: "=a" (_res) \
: "0" (__NR##name), "b" ((long)(arg1)), "c" ((long)(arg2))); \
// some code
__syscall_return(type, __res)
}
  • 7个宏分别可以适用于参数个数为0-6的系统调用 (超过6个参数的系统调用罕见)

  • __syscall[N]宏根据系统调用名创建name同名函数,通过 该函数即可访问系统调用

  • 大部分情况下用户程序都是通过库函数访问系统调用,调用 __syscall[N]系列宏通常由库函数完成
  • Linux2.6.19内核之后弃用

syscall

syscall:通过系统调用号相应参数访问系统调用

1
2
3
4
5
6
7
8
9
10
11
int syscall(int number, ...);

#include <unistd.h>
#include <sys/syscall.h>
#include <sys/types.h>

int main(int argc, char *argv){
pid_t tid;
// `SYS_[NAME]`是系统调用号常量
tid = syscall(SYS_getpid);
}

System Call Service Routine

系统调用服务例程/响应函数:系统调用的实际处理逻辑

  • 一般以sys_开头,后跟系统调用名

    • fork()的响应函数是sys_fork()
    • exit()的响应函数是sys_exit()
  • 系统调用可以看作是系统调用服务例程在内核中注册的 名,内核通过系统调用名寻找对应服务例程

    1
    2
    3
    4
    // arch/x86/entry/syscalls/syscall_64.tbl
    0 common read sys_read
    1 common write sys_write
    2 common open sys_open

参数传递

参数传递

  • 除系统调用号外参数输入同样存放在寄存器中
    • X86机器上,ebxecxedxesiedi按照顺序 存放前5个参数
    • 需要6个以上参数情况不多,此时应该用单独的寄存器存放 指向所有参数在用户空间地址的指针

参数验证

  • 系统调用需要检查参数是否合法有效、正确

    • 文件IO相关系统调用需要检查文件描述符是否有效
    • 进程相关函数需要检查PID是否有效
  • 用户指针检查,内核必须保证

    • 指针指向的内存区域属于用户空间:不能哄骗内核读取 内核空间数据
    • 指针指向的内存区域在进程地址空间:不能哄骗内核读取 其他进程数据
    • 进程不能绕过内存访问限制:内存读、写应被正确标记

访问系统调用

系统调用初始化

  • 系统调用初始化就是对陷入指令的初始化,在X86机器上就是 初始化INT 0x80指令

  • 系统启动时

    • 汇编子程序setup_idt()准备256项的idt表
    • start_kernel()trap_init()调用的C宏定义 set_system_gate(0x80, &system_call)设置0x80号 软中断服务程序为system_call
    • system_call就是所有系统调用总入口

系统调用上下文

  • 内核在执行系统调用时处于进程上下文

    • current指针指向当前任务,即引发系统调用的进程
  • 在进程上下文中,内可以休眠、被抢占

    • 能够休眠:系统调用可以使用内核提供的绝大部分功能, 方便内核编程
    • 能被抢占:新进程可以使用相同的系统调用
      • 保证系统调用可重入
  • 系统调用返回时,控制权依然在system_call()中,其会负责 切换到用户空间并让用户进程继续执行

系统调用返回值

errno错误码

  • 系统调用将错误码放入名为errno的全局变量中

    • 为防止和正常返回值混淆,系统调用不直接返回错误码
    • errno值只在函数发生错误时设置,若函数不发生错误, errno值无定义,并不置为0
      • 0值通常表示成功
      • 负值表示系统调用失败
        • 错误值对应错误消息定义在error.h
        • 可以通过perror()库函数翻译误码
    • 处理errno前最好将其存入其他变量中,因为在错误处理 过程中errno值可能会被改变
  • 系统调用具有明确的操作

ret_from_sys_call

  • ret_from_sys_call为入口的汇编程序段在Linux进程管理中 起到重要作用

    • 系统调用结束前、大部分中断服务返回前,都会跳转至此处 入口地址
    • 还处理中断嵌套、CPU调度、信号等
  • 给用户空间进程的返回值同样通过寄存器传递

    • X86机器上,存放在eax寄存器中

Linux系统调用、派生函数

进程管理

进程控制

  • fork:创建新进程
  • clone:按照指定条件创建子进程
  • execve:运行可执行文件
  • exit:终止进程
  • _exit:立即终止当前进程
  • getdtablesize:进程能打开的最大文件数
  • getpgid:获取指定进程组标识号
  • setpgid:设置指定进程组标识号
  • getpgrp:获取当前进程组标识号
  • setpgrp:设置当前进程组标识号
  • getpid:获取进程标识号
  • getppid:获取父进程标识号
  • getpriority:获取调度优先级
  • setpriority:设置调度优先级
  • modify_ldt:读写进程本地描述符
  • nanosleep:使指定进程睡眠
  • nice:改变分时进程的优先级
  • pause:挂起进程,等待信号
  • personality:设置进程运行域
  • prctl:对进程进行特定操作
  • ptrace:进程跟踪
  • sched_get_priority_max:取得静态优先级上限
  • sched_get_priority_min:取得静态优先级下限
  • sched_getparam:取得进程调度参数
  • sched_getscheduler:取得指定进程的调度策略
  • sched_rr_get_interval:取得按RR算法实调度的实时进程 时间片
  • sched_setparam:设置进程调度参数
  • sched_setscheduler:设置进程调度策略和参数
  • sched_yield:进程主动出让处理器,并添加到调度队列队尾
  • vfork:创建执行新程序的子进程
  • wait/wait3:等待子进程终止
  • watipid/wait4:等待指定子进程终止
  • capget:获取进程权限
  • capset:设置进程权限
  • getsid:获取会晤标识号
  • setsid:设置会晤标识号

进程间通信

  • ipc:进程间通信控制总控制调用

信号

  • sigaction:设置对指定信号的处理方法
  • sigprocmask:根据参数对信号集中的号执行阻塞、解除 阻塞等操作
  • sigpending:为指定被阻塞信号设置队列
  • sigsuspend:挂起进程等待特定信号
  • signal
  • kill:向进程、进程组发信号
  • sigvec:为兼容BSD设置的信号处理函数,作用类似 sigaction
  • ssetmask:ANSI C的信号处理函数,作用类似sigaction

消息

  • msgctl:消息控制操作
  • msgget:获取消息队列
  • msgsnd:发消息
  • msgrcv:取消息

管道

  • pipe:创建管道

信号量

  • semctl:信号量控制
  • semget:获取一组信号量
  • semop:信号量操作

内存管理

内存管理

  • brk/sbrk:改变数据段空间分配
  • mlock:内存页面加锁
  • munlock:内存页面解锁
  • mlockall:进程所有内存页面加锁
  • munlockall:进程所有内存页面解锁
  • mmap:映射虚拟内存页
  • munmap:去除内存映射页
  • mremap:重新映射虚拟内存地址
  • msync:将映射内存中数据写回磁盘
  • mprotect:设置内存映像保护
  • getpagesize:获取页面大小
  • sync:将内存缓冲区数据写回磁盘
  • cacheflush:将指定缓冲区中内容写回磁盘

共享内存

  • shmctl:控制共享内存
  • shmget:获取共享内存
  • shmat:连接共享内存
  • shmdt:卸载共享内存

文件管理

文件读写

  • tcntl:文件控制
  • open:打开文件
  • creat:创建新文件
  • close :关闭文件描述字
  • read:读文件
  • write:写文件
  • readv:从文件读入数据到缓存区
  • writev:将缓冲区数据写入文件
  • pread:随机读文件
  • pwrite:随机写文件
  • lseek:移动文件指针
  • _llseek:64位地址空间中移动文件指针
  • dup:复制已打开的文件描述字
  • dup2:按指定条件复制文件描述字
  • flock:文件加/解锁
  • poll:IO多路切换
  • truncat/ftruncate:截断文件
  • vumask:设置文件权限掩码
  • fsync:将内存中文件数据写入磁盘

文件系统操作

  • access:确定文件可存取性
  • chdir/fchdir:改变当前工作目录
  • chmod/fchmod:改变文件模式
  • chown/fchown/lchown:改变文件属主、用户组
  • chroot:改变根目录
  • stat/lstat/fstat:获取文件状态信息
  • statfs/fstatfs:获取文件系统信息
  • ustat:读取文件系统信息
  • mount:安装文件系统
  • umount:卸载文件系统
  • readdir:读取目录项
  • getdents:读取目录项
  • mkdir:创建目录
  • mknod:创建索引节点
  • rmdir:删除目录
  • rename:文件改名
  • link:创建链接
  • symlink:创建符号链接
  • unlink:删除链接
  • readlink:读取符合链接值
  • utime/utimes:改变文件的访问修改时间
  • quotactl:控制磁盘配额

驱动管理

系统控制

  • ioctl:IO总控制函数
  • _sysctl:读写系统参数
  • acct:启用或禁用进程记账
  • getrlimit:获取系统资源上限
  • setrlimit:设置系统资源上限
  • getrusage:获取系统资源使用情况
  • uselib:选择要使用的二进制库
  • ioperm:设置端口IO权限
  • iopl:改变进程IO权限级别
  • outb:低级端口操作
  • reboot:重启
  • swapon:开启交换文件和设备
  • swapoff:关闭交换文件和设备
  • bdflush:控制bdflush守护进程
  • sysfs:获取核心支持的文件系统类型
  • sysinfo:获取系统信息
  • adjtimex:调整系统时钟
  • getitimer:获取计时器值
  • setitimer:设置计时器值
  • gettimeofday:获取时间、时区
  • settimeofday:设置时间、时区
  • stime:设置系统日期和时间
  • time:获取系统时间
  • times:获取进程运行时间
  • uname:获取当前unix系统名称、版本、主机信息
  • vhangup:挂起当前终端
  • nfsservctl:控制NFS守护进程
  • vm86:进入模拟8086模式
  • create_module:创建可载入模块项
  • delete_module:删除可载入模块项
  • init_module:初始化模块
  • query_module:查询模型信息

网络管理

网络管理

  • getdomainname:获取域名
  • setdomainname:设置域名
  • gethostid:获取主机标识号
  • sethostid:设置主机标识号
  • gethostname:获取主机名称
  • sethostname:设置主机名称

Socket控制

  • socketcall:socket系统调用
  • socket:建立socket
  • bind:绑定socket到端口
  • connect:连接远程主机
  • accept:响应socket连接请求
  • send/sendmsg:通过socket发送信息
  • sendto:发送UDP信息
  • recv/recvmsg:通过socket接收信息
  • recvfrom:接收UDP信息
  • listen:监听socket端口
  • select:对多路同步IO进行轮询
  • shutdown:关闭socket上连接
  • getsockname:获取本地socket名称
  • getpeername:获取通信对方socket名称
  • getsockopt:获取端口设置
  • setsockopt:设置端口参数
  • sendfile:在文件端口间传输数据
  • socketpair:创建一对已连接的无名socket

用户管理

  • getuid:获取用户标识号
  • setuid:设置用户标识号
  • getgid:获取组标识号
  • setgid:设置组标识号
  • getegid:获取有效组标识号
  • setegid:设置有效组标识号
  • geteuid:获取有效用户标识号
  • seteuid:设置有效用户标识号
  • setregid:分别设置真实、有效的组标识号
  • setreuid:分别设置真实、有效的用户标识号
  • getresgid:分别获取真实、有效、保存过的组标识号
  • setresgid:分别设置真实、有效、保存过的组标识号
  • getresuid:分别获取真实、有效、保存过的用户标识号
  • setresuid:分别设置真实、有效、保存过的用户标识号
  • setfsgid:设置文件系统检查时使用的组标识号
  • setfsuid:设置文件系统检查时使用的用户标识号
  • getgroups:获取候补组标志清单
  • setgroups:设置候补组标志清单

通知内核

  • 一般系统调用都是通过软件中断向内核发请求,实现内核提供的 某些服务

    • 128号异常处理程序就是系统调用处理程序system_call()
  • 用户空间进程不能直接执行内核代码,需要通过中断通知内核 需要执行系统调用,希望系统切换到内核态,让内核可以代表 应用程序执行系统调用

  • 通知内核机制是靠软件中断实现,X86机器上软中断由int产生

    • 用户程序为系统调用设置参数,其中一个参数是系统调用 编号
    • 程序执行“系统调用”指令,该指令会导致异常
    • 保存程序状态
    • 处理器切换到内核态并跳转到新地址,并开始执行异常处理 程序,即系统调用处理程序
    • 将控制权返还给用户程序
  • arch/i386/kernel/head.s

  • init/main.c
  • arhc/i386/kernel/traps.c
  • include/asm/system.h

参数传递

  • _syscalN()用于系统调用的格式转换和参数传递

    • 参数数量为N的系统调用由_syscallN()负责
    • N取值为0-5之间的整数
  • 启动INT 0x80后,规定返回值送eax寄存器

    • 定义于include/asm/unistd.h,用于系统调用的格式转换 和参数传递

进程、线程、作业

Linux进程、线程

进程发展

  • Linux2.2内核

    • 进程通过系统调用fork()创建,新进程是原进程子进程
    • 不存在真正意义上的线程
    • 只默认允许4096个进程/线程同时运行
  • Linux2.4内核

    • 运行系统运行中动态调整进程数上限,进程数仅受制于物理 内存大小,最大16000
  • Linux2.6内核

    • 进程调度重新编写,引入slab分配器动态生成 task_struct
    • 最大进程数量提升至10亿
    • 线程框架重写
      • 引入tgid、线程组、线程各自的本地存储区
      • 得以支持NPTL线程库

线程/轻量级进程

  • Linux未真正实现、区分线程,在内核层面是特殊进程,是 “真正的轻量级进程”

    • “线程”和“进程”共享
      • 相同调度策略,处于同一调度层次
      • 相同数据结构进程标识符,位于相同进程标识符空间
    • “线程”与“进程”的区别在于
      • 线程没有独立的存储空间
  • 多线程即创建多个进程并分配相应的进程描述符 task_struct、指定其共享某些资源

    • 创建线程不会复制某些内存空间,较进程创建快
    • 在专门线程支持系统多线程中,系统会创建包含指向所有 线程的进程描述符,各线程再描述独占资源
  • 尽管Linux支持轻量级进程,但不能说其支持核心级线程

    • 则不可能在Linux上实现完全意义上的POSIX线程机制
    • 所以Linux线程库只能尽可能实现POSIX绝大部分语义,尽量 逼近功能
  • 线程在进程内共享某些资源

    • 打开的文件
    • 文件系统信息
    • 地址空间
    • 信号处理函数
  • 这里讨论的线程都是内核线程,即内核可感知、调度线程,不 包括程序自建线程

内核守护线程

kthreads pthreads
资源 无用户空间 共享完整虚拟寻址空间
状态 只工作在内核态 可在内核态、用户态之间切换
目的 维护内核正常工作 用户分配任务

内核守护线程:内核为维护正常运行创建、仅工作在内核态线程

  • 按作用可以分类

    • 周期性间隔运行,检测特定资源的使用,在用量超出或低于 阈值时采取行动
    • 启动后一直等待,直到系统调用请求执行某特定操作
  • 执行以下任务

    • 周期性将dirty内存页与页来源块设备同步:bpflush线程
    • 将不频繁使用的内存写入交换区:kswapd线程
    • 管理延时动作:kthreadd线程接手内核守护线程创建
    • 实现文件系统的事务日志
  • 内核守护线程只能工作在内核态

    • 没有用户空间,和内核共用一张内核页表
    • 只能使用大于PAGE_OFFSET部分的虚拟寻址空间,即进程 描述符中current->mm始终为空
    • 对4G主存的X86_32机器,只能使用最后1G,而普通pthreads 可以使用完整虚拟寻址空间
  • 内核守护线程名往往为k开头、d结尾

特殊内核守护线程

  • Linux内核启动的最后阶段,系统会创建两个内核线程
  • init:运行文件系统上一系列init脚本,并启动shell 进程

    • 是所有用户进程的祖先,pid为1
  • kthreadd:内核启动完成之后接手内核守护线程的创建

    • 内核正常工作时永不退出,是死循环,pid为2
    • 载入内核模块时即需要调用其创建新内核守护线程

进程状态

1
2
3
4
5
6
7
8
// <kernel/include/linux/sched.h>
#define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2
#define __TASK_STOPPED 4
#define __TASK_TRACED 8
#define EXIT_ZOMBIE 16
#define TASK_DEAD 64

process_status

  • 状态虽然有很多种,但总是TASK_RUNNING -> 非, 即使进程在TASK_INTERRUPTIBLE状态被kill,也需要先唤醒 进入TASK_RUNNING状态再响应kill信号进入TASK_DEAD
  • TASK_RUNNING:可执行,正在执行或在运行队列中等待执行

    • 同一时刻可能有多个进程处于可执行态,位于运行队列中 等待进程调度器调度
  • TASK_INTERRUPTIBLE:正在阻塞,等待某些条件达成

    • 条件达成后内核会把进程状态设置为运行
    • 此状态进程也会因为接收到信号而提前唤醒准备运行
    • 系统中大部分进程都此状态
  • TASK_UNINTERRUPTILBE:不响应异步信号,即使接收到信号 也不会被唤醒或准备投入运行

    • 不可中断是指进程不响应异步信号,而不是指CPU不响应 中断
    • 内核某些处理流程是不可被打断的,如:内核和硬件设备 交互被打断会导致设备进入不可控状态,因此需要此状态
  • __TASK_TRACED:被其他进程跟踪

    • 开发中进程停留在断点状态就是此状态,如:通过ptrace 对调试程序进行跟踪
    • 此状态进程只能等待调试进程通过ptrace系统调用执行 PTRACE_CONTPTRACE_DETACH等操作才能恢复到 TASK_RUNNING状态
  • __TASK_STOPPED:停止执行,没有也不能投入运行

    • 通常发生在接收到SIGSTOPSIGSTPSIGTTINSIGTTOU等信号
    • 向此状态进程发送SIGCONT信号可以让其恢复到 TASK_RUNNING状态
  • TASK_DEAD:退出状态,即将被销毁

  • EXIT_ZOMBIE/TASK_ZOMBIE:进程已结束但task_struct未 注销

    • 进程退出过程中处于TASK_DEAD状态,进程占有的资源将 被回收,但父进程可能会关心进程的信息,所以 task_struct未被销毁

内核态、用户态

  • 系统设计角度:为不同的操作赋予不同的执行等级,与系统相关 的特别关键的操作必须有最高特权程序来完成

    • 运行于用户态:进程可执行操作、可访问资源受到限制
    • 运行于内核态:进程可执行任何操作、使用资源无限制
  • 内存使用角度(虚拟寻址空间,X86_32位系统,最大4GB主存)

    • 内核空间:最高的1G,所有进程共享
      • 包含系统堆栈:2页面,即8K内存,低地址中存放 task_struct
      • 进程运行于内核空间时使用系统堆栈、处于内核态
    • 用户空间:剩余3G
      • 包含用户堆栈
      • 进程运行于用户空间时使用用户堆栈、处于用户态

    virtual_address_space

  • 内核态的逻辑

    • 进程功能和内核密切相关,进程需要进入内核态才能实现 功能
    • 应用程序在内核空间运行、内核运行于进程上下文、陷入 内核空间,这种交互方式是程序基本行为方式
  • 用户态进入内核态的方式

    • 系统调用,如:printf函数中就是调用write函数
    • 软中断,如:系统发生异常
    • 硬件中断,通常是外部设备的中断

    process_calling_structure

  • 进程或者CPU在任何指定时间点上活动必然为

    • 运行于用户空间,执行用户进程
    • 运行于内核空间,处于进程上下文,代表某特定进程执行
    • 运行于内核空间,处于中断上下文,与任何进程无关,处理 特点中断

Linux进程数据结构

task_struct

1
2
3
4
5
6
7
8
9
10
11
12
13
// <kernel/include/linux/sched.h>
struct task_struct{
volatile long state; // -1:不可运行,0:可运行,>0:已中断
int lock_depth; // 锁深度
unsigned int policy; // 调度策略:FIFO,RR,CFS
pid_t pid; // 线程ID
pid_t tgid; // 线程组ID,2.6内核中引入
struct task_struct *parent; // 父进程
struct list_head children; // 子进程
struct list_head sibling; // 兄弟进程
struct task_struct *group_leader;
struct list_head thread_group;
}

task_struct

  • 内核使用任务队列(双向循环链表)维护进程(描述符)

  • task_struct:进程描述符,包含进程的所有信息,包括

    • 进程状态
    • 打开的文件
    • 挂起信号
    • 父子进程

ID

  • pid:字面意思为process id,但逻辑上为线程ID
  • tgid:字面意思为thread group id,但逻辑上为 进程ID
1
2
3
4
5
6
7
// <kernel/timer.c>
asmlinkage long sys_getpid(void){
return current->tgid;
}
asmlinakge long sys_gettid(void){
return current->pid;
}

线程关系

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
// <kernel/fork.c>
copy_process{
// some code
p->tgid = p->pid;

// 创建线程时
if (clone_flags & CLONE_THREAD)
// 从父进程获取`tgid`,归属同一线程组
p->tgid = current->tgid;

// some code
// 初始化`group_leader`、`thread_group`
p->group_leader = p;
INIT_LIST_HEAD(&p->thread_group);

// some code

// 创建线程时
if (clone_flags & CLONE_THREAD){
// `group_leader`设置为父进程`group_leader`
// 即保证`group_leader`指向首个线程`task_struct`
p->group_leader = current->group_leader;
// 通过`thread_group`字段挂到首个线程的`thread_group`队列中
list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group);

// some code
}

if(likely(p->pid)){
// some code
// 仅首个线程才会通过`tasks`字段挂入`init_task`队列中
if(thread_group_leader(p)){
//...
list_add_tail_rcu(&p->tasks, &init_task, tasks);
}
}
}

线程组退出

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
// <kernel/exit.c>
NORET_TYPE void do_group_exit(int exit_code){
BUG_ON(exit_code & 0x80);

// `current->signal`由线程组中所有线程共享
// 若调用此方法线程`SIGNAL_GROUP_EXIT`标志已被设置,说明
// 其他线程已经执行过此方法,已通知线程组中所有线程
// 退出,则可以直接执行`do_exit`
if(current->signal->flags & SIGNAL_GROUP_EXIT)
exit_code = current->signal->group_exit_code;

// 否则通知线程组中所有线程退出
else if(!thread_gropu_empty(current)){
struct signal_struct * const sig = current->signal;
struct sighand_struct * const sighand = current->sighand;
spin_lock_irq(&sighand->siglock);

// another thread got here before we took the lock
if(sig->flags & SIGNAL_GROUP_EXIT)
exit_code = sig->group_exit_code;
else{
sig->group_exit_code = exit_code;
zap_other_threads(current);
}
spin_unlock_irq(&sighand->sigloc);
}

do_exit(exit_code);
}

// <kernel/signal.c>
void zap_other_threads(struct task_struct *p){
struct task_struct *t;

// 设置`SIGNAL_GROUP_EXTI`标志
p->signal->flags = SIGNAL_GROUP_EXIT;
p->signal->group_stop_count = 0;

if(thread_group_empty(p))
return;

for (t=next_thread(p); t != p; t=next_thread(t)){
// don't bohter with already dead threads
if (t->exit_state)
continue;

// 为每个线程设置`SIGKILL`信号
sigaddset(&t->pending.signal, SIGKILL);
signal_wake_up(t, 1);
}
}

// <include/linux/sched.h>
static inline struct task_struct *next_thread(const struct task_struct *p){
return list_entry(rcu_dereference(p->thread_group.next),
struct task_struct, thread_group);
}

Slab分配器

process_slab

  • slab分配器把不同对象类型划分为不同高速缓存组,如: task_structinode分别存放

    • 高速缓存又会被划分为slab
    • slab由一个或多个物理上连续的页组成
  • 申请数据结构时

    • 先从半满的slabs_partial中申请
    • 若没有半满,就从空的slabs_empty中申请,直至填满 所有
    • 最后申请新的空slab
  • slab分配器策略优点

    • 减少频繁的内存申请和内存释放的内存碎片
    • 由于缓存,分配和释放迅速

thread_info

1
2
3
4
5
6
7
8
9
10
// <asm/thread_info.h>
struct thread_info{
struct task_struct *task;
struct exec_domain *exec_domain;
usigned long flags;
__u32 cpu;
int preempt_count;
mm_segment_t addr_limit;
struct restart_block restart_block;
}
  • 内核中对进程操作都需要获得进程描述符task_struct指针, 所以获取速度非常重要
    • 寄存器富余的体系会拿出专门的寄存器存放当前 task_struct的指针
    • 寄存器不富余的体系只能在栈尾创建thread_info结构, 通过计算间接查找

进程创建

  • 继承于Unix,Linux进程创建使用两个函数分别完成,其他如Win 可能都是通过一个方法完成
  • fork函数:拷贝当前进程创建子进程

    • 子进程、父进程区别仅在于PID、PPID和少量资源
  • 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
SYSCALL_DEFINE0(fork){
return do_fork(SIGCHLD, 0, 0, NULL, NULL);
}
SYSCALL_DEFINE0(vfork){
return _do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD,
0, 0, NULL, NULL, 0);
}
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr){
return _do_fork(clone_flags, stack_start, stack_size,
parent_tidptr, child_tidptr, 0);
}
long _do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr,
unsigned long tls){
// some code
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace, tls);
// some code
}
  • forkvfork最终都是通过调用_do_fork实现,仅传参 不一致

    • 首个参数为clone_flags,最终被copy_process用于 真正的拷贝执行
  • 通过系统调用clone()创建线程

    • 同创建进程系统调用fork()vfork()一样,最终调用 do_fork方法,但传递和进程创建时不同的flag,指明 需要共享的资源

      1
      CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGNAND

fork

fork():子进程是父进程的完整副本,复制了父进程的资源, 包括内存内容、task_struct

  • 子进程拷贝父进程的数据段、代码段

    • 同一变量的虚拟地址相同(物理地址不同)
  • 利用copy-on-write优化效率

    • 内核创建子进程时不复制父进程的地址空间,而是只读共享 父进程空间数据
    • 只有子进程需要写数据时才会拷贝到子进程
  • 页表:存放给从逻辑页号到物理页帧/块号地址的映射

Unix傻瓜式进程创建

  • 内核原样复制父进程整个地址空间,并分配给子进程,效率低

    • 为子进程页表分配页帧
    • 为子进程页分配页帧
    • 初始化子进程页表
    • 把父进程页复制到子进程相应页中
  • 大部分情况下复制父进程页无意义

    • 子进程会载入新的程序开始运行
    • 丢弃所继承的地址空间

Copy-on-Write

copy-on-write思想简单:父进程、子进程共享页帧

  • 共享页帧不能被修改,父进程、子进程试图写共享页帧时产生 page_fault异常中断

  • CPU执行异常处理函数do_wp_page()解决此异常

    • 对导致异常中断的页帧取消共享操作
    • 为写进程复制新的物理页帧,使父、子进程各自拥有内容 相同的物理页帧
    • 原页帧仍然为写保护:其他进程试图写入时,内核检查进程 是否是页帧的唯一属主,如果是则将页帧标记为对此进程 可写
  • 异常处理函数返回时,CPU重新执行导致异常的写入操作指令

  • copy-on-write:多个呼叫者同时要求相同资源时,会共同 取得相同指针指向相同资源,直到某个呼叫者尝试修改资源时, 系统才给出private copy,避免被修改资源被直接察觉,此 过程对其他呼叫者transparent

vfork

vfork():子进程直接共享父进程的虚拟地址空间、物理空间

  • vfork被设计用以启动新程序

    • 内核不创建子进程的虚拟寻址空间结构
    • 进程创建后应立即执行exec族系统调用加载新程序,替换 当前进程
    • exec不创建新进程,仅用新程序替换当前进程正文、 数据、堆、栈
  • 在子进程调用exec函数族、_exit()exit()前,子进程 在父进程的地址空间中运行

    • 二者共享数据段,子进程可能破坏父进程数据结构、栈
    • 父进程地址空间被占用,因此内核会保证父进程被阻塞, 即vfork会保证子进程先运行
  • 应确保一旦调用vfork

    • 子进程不应使用return返回调用处,否则父进程又会 vfork子进程
    • 子进程不应依赖父进程进一步动作,否则会导致死锁
    • 子进程需避免改变全局数据
    • 若子进程改变了父进程数据结构就不能调用exit函数

clone

clone:可有选择地继承父进程资源

1
int clone(int (fn)(void), void * child_stack, int flags, void * args);
  • clone通过众多参数有选择地创建进程

    • 创建LWP/线程
    • 创建兄弟进程
    • 类似vfork创建和父进程共享虚拟寻址空间
  • 参数说明

    • fn:函数指针
    • child_stack:为子进程分配的系统堆栈空间
    • flags:描述需要从父进程继承的资源,如下
    • args:传给子进程的参数

Flags

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#define CSIGNAL		0x000000ff		// signal mask to be setn at exit
#define CLONE_VM 0x00000100 // set if VM shared between process
#define CLONE_FS 0x00000200 // set if fs info shared between processes
#define CLONE_FILES 0x00000400 // set if open files shared between processes
#define CLONE_SIGHAND 0x00000800 // set if signal handlers and blocked signals shared
#define CLONE_PTRACE 0x00002000 // set if we want to let tracing continue on the child too
#define CLONE_VFORK 0x00004000 // set if the parent wants the child to wake it up on mm_release
#define CLONE_PARENT 0x00008000 // set if we want to have the same parent as the cloner
#define CLONE_THREAD 0x00010000 // same thread group?
#define CLONE_NEWS 0x00020000 // new namespace group?
#define CLONE_SYSVSEM 0x00040000 // share system V SEM_UNDO semantics
#define CLONE_SETTLS 0x00080000 // create a new TLS for the child
#define CLONE_PARENT_SETTID 0x00100000 // set the TID in the parent
#define CLONE_CHILD_CLEARTID 0x00200000 // clear TID in the child
#define CLONE_DETEACHED 0x00400000 // unused
#define CLONE_UNTRACED 0x00800000 // set if the tracing process can't force `CLONE_PTRACE` on this clone
#define CLONE_CHILD_SETTID 0x01000000 // set the TID in the child
#define CLONE_STOPPED 0x02000000 // start in stopped state
#define CLONE_NEWUTS 0x04000000 // new utsname group
#define CLONE_NEWIPC 0x08000000 // new ipcs
#define CLONE_NEWUSER 0x10000000 // new user namespace
#define CLONE_NEWPID 0x20000000 // new pid namespace
#define CLONE_NEWNET 0x40000000 // new network namespace
#define CLONE_IO 0x80000000 // clone into context

线程库

  • POSIX标准要求:线程不仅仅是共享资源即可,其需要被视为 整体
    • 查看进程列表时,一组task_struct需要被展示为列表中 一个节点
    • 发送给进程信号,将被一组task_struct共享,并被其中 任意一个线程处理
    • 发送给线程信号,将只被对应task_struct接收、处理
    • 进程被停止、继续时,一组task_struct状态发生改变
    • 进程收到致命信号SIGSEGV,一组task_struct全部退出

LinuxThread线程库

LinuxThread线程库:Linux2.6内核前,pthread线程库对应实现

  • 特点
    • 采用1对1线程模型
    • 通过轻量级进程模拟线程
    • 线程调用由内核完成,其他线程操作同步、取消等由核外 线程库完成
    • 仅通过管理线程实现POSIX以上5点要求中最后一点

管理线程

管理线程:为每个进程构造、负责处理线程相关管理工作

  • 管理线程是主线程的首个子线程

    • 进程首次调用pthread_create创建线程时会创建、启动 管理线程
  • 管理线程负责创建、销毁除主线程外线程,成为LinuxThread 的性能瓶颈

    • 从pipe接收命令创建线程
    • 子线程退出时将收到SIGUSER1信号(clone时指定), 若不是正常退出,则杀死所有子线程并自杀
    • 主循环中不断检查父进程ID,若为1说明原父线程退出并 被托管给init线程,则杀死所有子进程并自杀
  • 通过LWP模拟线程存在的问题

    • LWP不共享进程ID
    • 某些缺省信号难以做到对所有线程有效,如:SIGSTOPSIGCONT无法将整个进程挂起
    • 线程最大数量收到系统总进程数限制
    • 管理线程是性能瓶颈,一旦死亡需要用户手动清理线程、 无人处理线程创建请求
    • 同步效率低,通过复杂的信号处理机制进行同步
    • 与POSIX兼容性差

Naive POSIX Thread Library

NPTL:Linux2.6内核重写线程框架的基础上引入的pthread线程库

  • 本质上还是利用LWP实现线程的1对1线程模型,但结合新的线程 框架实现了POSIX的全部5点要求

    • 线程组tgid引入体现task_struct代表进程还是线程
    • task_struct维护两套signal_pending
      • 线程组共享signal_pending:存放kill发送信号, 任意线程可以处理其中信号
      • 线程独有signal_pending:存放pthread_kill发送 信号,只能由线程自身处理
    • 收到致命信号时,内核会将处理动作施加到线程组/进程中
  • 但也存在一些问题

    • kill未展示的LWP会杀死整个进程
  • RedHat开发,性能远高于LinuxThreads,需要内核支持

Next Generation Posix Threads for Linux

NGPT:基于GNU Portable Threads项目的实现多对多线程模型

  • IBM开发,性能介于LinuxThread、NPTL间,2003年终止开发

Parallel

并发和并行

  • Parallel:并行,同时做多件事情,关于执行、实现
  • Concurrent:并发,能够处理多件事情,关于结构、逻辑
  • 并发问题可以使用并行方式解决,也可以串行解决

    • 100并发任务同时运行在4核CPU上,最多可能有4个并发任务 并行处理,其余只能是串行处理
  • 并行角度:硬件技术的物理限制瓶颈

    • 单计算核心能力不足,所以需要多核并行运算
    • 进程、线程可认为是实现并行的基本逻辑实体
  • 并发角度:程序执行的逻辑重用需求

    • 程序要求重用一组逻辑,所以需要将一组指令集打包,重复 调用该组指令集
    • 子程序、协程可认为是方便重用的基本逻辑实体,因此更 应是语言内建机制
      • 子程序:无状态保存,同样重入得到同样结果
      • 协程:有保存状态,重入会改变协程状态,结果可能 不同
  • 线程作为任务执行的实体,可以认为是子程序、协程的具体执行

    • 内核线程作为可以独立执行的实体,逻辑上更会被设计为 完成独立任务,即没有保存状态需求,因此多是子程序的 具体执行
    • 用户线程则用程序负责调度,二者执行实例均可
    • 某种意义上线程、子程序是对应的执行实体、逻辑实体

子程序、协程

  • 子程序可以看作时协程的特例
    • 只有一个状态,每次进入时局部状态重置
    • 唯一入口点
  • 协程可视为子程序的组成
    • 维护自身状态,所以逻辑上不独立 ,应该是作为被 调用对象
    • 对于每次返回部分结果值的协程(也称生成器迭代器), 可以直接视为类似链表之类的数据结构 (在某些语言中可以所有数据都是类,从这个角度这也都是 统一的)
子程序 协程
生命周期 后进先出 完全取决于需要
入口点 起始处 起始处、yield返回出口点
返回值 调用结束后返回全部 可每次yield返回部分值
  • 现代指令集通常提供对调用栈的指令支持,便于实现可递归 调用的子程序,在提供续体的语言环境(如Scheme),恰好 可用此抽象状态表示实现协程

Subroutine/Procedure/Function/Routine/Method

子程序:打包为整体、用于执行特定任务的指令集序列

  • 子程序是依赖可重入能力的弱化版本

    • 一旦唤醒,于起始点开始执行
    • 一旦退出,子程序结束
    • 子程序实例只返回一次,两次激活间不保存状态
  • 子程序中局部变量在每次调用/重入函数时都是相同的

    • 相同输入得到相同输出
  • procedure:过程,有时特指无返回值、仅有副作用

线程安全

线程安全:子程序在多线程环境调用时,能够正确处理多个线程之间 的共享变量,使程序功能能正确完成

  • 线程安全函数应该为每个调用其的线程分配专门空间,存储需要 单独保存的状态

  • Atomicity:原子性,操作不会被线程调度机制打断,一旦 开始就会运行到结束,中间不会有任何线程切换

    • 可以通过locksynchronized确保原子性
  • Visibility:可见性,某线程修改变量值后,其他线程能够 立刻感知
    • 一般可以通过volatile保证可见性,强制要求被修改值 从寄存器同步至主存
    • locksynchronized也可以通过限制其他线程访问变量 的方式保证可见性
  • Ordering:有序性/一致性,程序按照代码顺序执行
    • 可以通过volatile保证一定的有序性
    • 也可通过locksynchronized提供单线程执行环境保证 有序性

Instruction Reorder

指令重排:编译器对无相互依赖的指令重新排序执行

  • as-if-serial语义:指令可以为优化而重排序,但是必须保证 最终执行结果不变

    • 规则:重排序过程不破坏数据依赖关系
    • 只能保证单线程执行结果有效,但不保证多线程并发执行 的正确性

    instruction_reorder_asif

  • happens-before原则:保证前后两个操作间不会被重排序,

    • 程序次序规则:线程中每个操作happens-before该线程中 任意后续操作
    • 锁定规则:锁的解锁happens-before加锁
    • volatile变量规则:volatile变量写操作happens-before 其读操作
    • 传递规则:若A happens-before B、B happens-before C,则A happens-before C
    • 线程启动规则:线程对象启动happens-before线程中每个 动作
    • 线程中断规则:线程中断方法的调用happens-before被 中断线程代码检测到的中断事件的发生
    • 线程终结规则:线程中所有操作happens-before线程的 终止检测
    • 对象终结规则:对象的初始化happens-beforefinal 方法的开始
  • happens-before原则被JVM用于规定(跨线程)操作之间偏序 关系,若操作之间的关系可以由此原则退出,则两个操作有序

Reentrant

  • A computer program or routine is described as reentrant if it can be safely executed concorrently; that is, the routine can be re-entered while it is already running

可重入函数:对于相同(合法)的函数参数,多次重复调用(包括 执行过程中被中断再重入)结果总是可预期的

  • 可重入需要满足条件

    • 不在函数内部使用静态或全局数据,所有数据都由函数 调用者提供
      • 全局变量区
      • 中断向量表
    • 使用本地数据,或制作全局数据的本地拷贝保护全局数据
    • 不返回静态或全局数据
    • 不调用不可重入函数
  • 不可重入后果主要体现在信号处理函数这样需要重入情况中, 若在信号处理函数中使用了不可重入函数,则可能导致程序错误

  • 可重入函数总是线程安全的,反之不一定成立

    • 线程安全可以通过“并发不冲突”实现
    • 可重入则要求“并行不冲突”

Coroutine

协程:为非抢占式多任务产生子程序的程序组件,允许执行过程 中挂起、恢复

  • 挂起、恢复:协程可以通过yield(让步)调用其他协程暂时 退出,之后可在退出位置恢复执行

    • 从协程角度看,这是调用其他协程而不是退出
    • 但实际是各协程之间是对称的,而不像子程序调用 中主调-被调关系
    • 这即暗含
      • 协程可包含多个入口点
      • 允许在不同入口点暂停、开始执行程序
  • 局部状态维护:协程实例保持上次退出时状态

    • 则协程被唤醒时状态可能不同
    • 可能同时有多个给定协程实例
  • 协程将原在子程序外、输入状态管理工作交由自身逻辑维护
  • 原生不支持协程的语言也可以使用循环等构建
  • 经典状态机、对象已经具有协程特性

用途

  • 协程可以简化异步代码的实现,使得需要使用异步+回调的代码 可以使用看似同步方式写出

    • 协程本身只涉及状态保存、过程重入,和并发/异步无关系
    • 但协程本身蕴含的状态保存使得状态切换几乎无成本,适合 高并发任务
  • 协程在线程中调度完全由用户控制,可以视为用户态轻量级线程

    • 避免陷入无效内核级别上下文切换造成的性能损失
    • 较线程在IO密集任务上性能上更好

进程、线程

  • 这里仅讨论理论上的异同,不考虑各平台具体实现
  • 进程:具有独立功能的程序关于某数据集合的一次运行活动
  • 线程/子程序:进程内某特定任务的执行活动
  • 协程:推广协作式多任务的子程序
Process Thread Coroutine
调度、创建、切换、维护 内核(系统) 内核、自身 自身
切换开销、速度 大、慢 小、快
易用性 需要考虑进程退出、僵尸进程 只需要管理进程即可 无需管理
资源共享 独立 同进程内线程共享资源 除局部变量外均共享
通信 IPC较复杂:环境变量、文件、系统端口 较简单:共享内存 结果调用
移植性
健壮性 好,进程死亡不影响其他进程 差,线程死亡会导致进程(及线程)死亡
  • 进程、线程、协程可以看作是控制权逐渐从系统已经移交到自身 的过程
  • 这里的协程强调其在实现方面的资源、调度特点,其与子程序间 功能差异参加cs_program/parallel/implementation
  • 通信:参见cs_program/parallel/#todo
  • 移植性:基于进程分支多进程和windows模型有很大冲突,往往 不能在windows平台上使用

Process

进程:具有独立功能的程序关于某数据集合的一次运行活动

  • 进程是处于运行期的程序和相关资源的总称

    • 程序:代码、指令
    • 运行:对CPU的占用,逐渐发展为线程,标识进程中指令的执行
    • 资源:执行上下文,由进程内线程共享
  • 从系统调度方面看

    • 进程是系统进行资源分配、调度的独立单位
    • 在系统中有进程控制块(进程描述符)描述进程相关信息, 系统通过此控制块控制系统相关行为
  • 从资源分配方面看

    • 有独立的存储空间(虚拟寻址空间)
      • 独享的用户空间
      • 进程专用的“共享内核空间”
    • 可执行的程序代码
  • 线程可能对系统是可感知的,则进程不定是资源分配的基本 单位
  • Linux线程实现即类似进程,但不包含独立存储空间

调度角度

  • 内核跟踪进程运行所需的状态信息(上下文)

    • 主存、虚拟内存内容
    • 寄存器文件值
    • 文件句柄
  • 调度:分配CPU执行进程

    • 内核决定CPU控制权在进程间的转移
  • 上下文切换:进程状态的记录、恢复、切换

    • 保存当前进程上下文、恢复新进程上下文
    • 通过处理器在进程间切换,实现单个CPU“看似”并发执行 多个进程
    • 上下文进程间切换开销比较大,但相对比较稳定安全

资源角度

  • 独立内存空间/虚拟地址空间:每个进程“独占的”使用 内存、看到一致的存储器

process_virtual_address_space_structure

  • 用户空间

    • 程序代码、数据:对所有进程,代码从同一固定位置开始, 直接按照可执行目标文件的内容初始化
    • (运行时)堆:可在运行时动态扩展、收缩
    • 共享库:共享库代码和数据,如:C标准库、数学库
    • 栈:用于实现函数调用,运行时动态扩展、收缩,位于虚拟 地址空间顶部
  • 内核空间

    • 内核虚拟存储器:内核总是驻留在内存中,为其保留地址 空间顶部
    • 不允许程序读写区域内容或直接调用内核代码定义的函数

Thread

线程:进程执行实体,进程中包含指令的执行活动

调度角度

  • 线程是CPU调度、分派的基本单位,有时被称为轻量级进程 (在Linux系统中也是按照轻量级进程实现)
  • 线程引入逻辑

    • 进程内部可能存在多个不同task,task需要共享进程数据
    • 同时task操作的数据具有独立性,多个task不需要按照时序 执行
    • task间需根据不同策略进行调度,因此产生了线程概念, 并被引入作为内核调度基本单位
  • 线程:比进程更小的、能独立运行的基本单位

    • 线程能/是“独立运行”,但不一定能被内核感知到,也一定由 内核调度
    • 只能说线程是针对某个task的执行活动

资源角度

thread_virtual_address_space_structure

  • 线程运行在进程的上下文中,共享同样代码和全局数据

    • 进程代码段、公有数据
    • 进程打开的文件描述符、信号的处理器
    • 进程当前目录
    • 进程用户ID、进程组ID
  • 线程还独享某些个性以实现并发性

    • 线程ID:进程中唯一标识
    • 寄存器组值:线程间并发运行,线程有不同运行线索, 切换时需要保存当前线程的寄存器集合状态
    • 线程堆栈:独立函数堆栈保证线程内函数调用可以正常 执行,不受其他线程影响
    • 错误返回码:线程的系统调用错误可能未及时处理,独立 错误返回码避免其被其他线程修改
    • 线程的信号屏蔽码:线程感兴趣的信号不同,因此信号 屏蔽码应由自己管理
    • 线程优先级:线程需要像进程被调度,需要有相应的优先级

线程实现理论

User-Level Thread

用户级线程:由用户程序自身负责支持、调度

  • 特点

    • 相当于实现自己的线程调度内核,实现线程数据结构、 创建、销毁、调度维护
    • 线程运行在内核(可感知的)进程内
  • 优点

    • 即使系统不支持线程,也可通过库函数支持在系统中实现 真实的多线程
    • 线程只在用户态,减少内核态到用户态切换开销
  • 缺点:线程对系统透明,对系统每个进程只有一个线程, 系统直接调用进程

    • 当线程进行系统调用而阻塞时,系统会阻塞整个进程
    • 用户空间没有时钟中断机制,若线程长时间不释放CPU, 会导致阻塞其他线程

Kernel-Level Thread

内核级线程:系统内核支持的线程,通过内核完成线程切换

  • 优点/特点:系统负责线程的创建、销毁、调度、维护

    • 内核通过操纵调度器对线程进行调度,并负责将线程 的任务映射到各个处理器上
    • 程序可以直接使用系统调用已实现线程,无需实现线程 调度、对CPU资源抢占使用
  • 缺点:内核线程需要内核支持

    • 创建、销毁、调度、维护往往都需要系统调用,代价较高
    • 需要消耗内核资源,不能大量创建

线程调度模型

  • X对Y是指X task对应Y内核调度单位

N对1模型

thread_model_n_versus_1

N对1模型:线程库有实现用户级线程,内核未实现内核级线程

  • 系统只负责调用进程
  • 线程对系统透明,由进程自身负责调用
  • 此模型中内核没有实现内核级线程,所以内核调度单位就是进程 S

    1对1模型

thread_model_1_versus_1

1对1模型:线程库未实现用户级线程,内核实现有内核线程

  • 程序(逻辑)层面

    • 创建执行独立task的线程
    • 程序创建的线程直接由内核负责调度
  • 内核(实现)层面

    • 每次创建线程都是调用系统调用创建内核级线程
    • 可视为每个“用户创建的线程”同内核级线程绑定,二者一一 对应
  • 此模型中内核调度单位就是内核级线程
  • 此模型中不存在上述定义的用户级线程,图中Thread实际上 应该就是Kernel Thread,仅拆分出来表示是程序创建的线程

M对N模型

thread_model_m_versus_n

M对N模型:线程库实现有用户级线程,系统也实现有内核级线程

  • 程序(逻辑)层面

    • 创建执行独立task的用户级线程
    • 创建可独立被内核调度的内核级线程
    • 将若干和用户线程同内核线程相关联,即task组总体由内核 负责调度,task组内task由程序自身负责调度
  • 内核(实现)层面

    • 调用系统调用创建内核级线程
    • 内核线程执行指令中包含用户线程创建、调度指令
  • 优点

    • 用户级线程创建、切换、析构等均在用户空间中,依然 廉价,支持大规模用户线程并发
    • 内核级线程作为用户线程在内核中调度、执行的桥梁, 降低整个进程被完全阻塞的风险
  • 此模型中内核调度单位为内核级线程、task单位为用户线程, 二者比例不定
  • 有的说法里面会使用Light Weighted Process表示内核级线程

通用调度算法

  • 耗时相差不大的task队列总是比较好处理,难以调度的task是

    • 耗时相差大
    • IO任务、计算任务混合
  • 内核调度除考虑任务(线程)外,还会考虑进程因素

    • Gang Scheduling:尽量将同进程中线程同时调度,而非 随机从多个进程中挑选CPU数量线程调度
    • Space Sharing:将CPU划分,各进程仅允许占用部分CPU 执行并发
  • 从任务角度,调度需要考虑

    • Responsiveness
    • Schedule Overload
    • Starvation-Freedom:饥饿避免
    • Fairness

First-In-First-Out

先进先出:按task在队列中的顺序依次调用,执行完task再执行下个 task,仅在task结束后才会切换task

  • 优点

    • 最少task切换开销
    • 最大吞吐量(总处理效率)
    • 朴实公平
  • 缺点

    • 平均相应时间高

Shortest task First/Shortest Remained Time task

最短耗时task优先:有限调度耗时短task

  • 优点

    • 平均相应时间短:长耗时task不断推移,必然统计出较短 平均响应时间
  • 缺点

    • 不公平,长耗时task难被调度,容易饥饿
    • 频繁task切换,调度额外开销大

Round Robin

时间片轮转:给队列中每个task时间片,时间片结束之后task移至 队列末尾,切换到执行下个task

  • 优点

    • 每个task可以得到公平调度
    • 耗时短task即使在耗时长task之后也可以较快得到执行
  • 缺点

    • task切换引起的调度开销大,需要多次切换task上下文
    • 时间片不好设置
      • 时间片足够小则退化为SFJ
      • 时间片足够大则退化为FIFO
    • 需要知道task(剩余)执行时间

(Weighted) Max-Min Fairness

(带权重的)最大最小公平:资源按照需求递增的顺序分配,不存在 需求得到资源超过自身需求,未得到满足得到需求等价分享资源

  • 具体方案
    • 每轮开始将资源按照权重分配
    • 若需求大于被分配资源则推迟执行,进入下轮
    • 若需求小于被分配资源则执行,并将多余资源继续按照权重 分配给无法执行资源

Multi-level Feedback Queue

多级反馈队列:监控task处理耗时,若task未用尽分配资源则提高 优先级,否则降低其优先级

  • 具体方案

    • task、分片时长具有相对应的不同优先级
      • 分片时长越长优先级越低
      • 高级优先级task可以抢占低优先级task
      • 新task位于高优先级task
    • 同一优先级task使用Round Robin (事实上仅有最低优先级task使用Round Robin算法, 其他优先级都是FIFO)
      • 时间片用完后task结束则正常退出系统,否则优先级 下滑一等级
      • 若是主动让出CPU(IO等),则停留在当前优先级或 提升
  • 多核场景

    • 应该为每个CPU分配单独MFQ,同时采用 Affinity Scheduling保证task尽量同一CPU核上执行, 避免CPU cache频繁失效
    • 若共用MFQ,则容易出现
      • 随CPU增长,对MFQ锁争抢严重
      • task每次执行位于的CPU不同,CPU Cache需要在不同 CPU间转移

同步问题

生产者-消费者问题/缓存绑定问题

  • 生产者生成数据放入缓存,消费者从缓存获取、移除、消费数据 ,问题核心在于保证不让生产者在缓存已满时放入数据、不让 消费者在缓存为空时读取数据
  • 若缓存满:生产者者停止工作
  • 若缓存空:消费者停止消费
  • 消费者从缓存中取走数据,通知生产者工作
  • 生产者向缓存中放入数据,通知消费者消费
  • 不完善的解决方案会造成死锁:生产者、消费者均等待对方唤醒

信号量解决方案

  • mutex信号量:互斥信号量,确保只有一个生产者、消费者 操作缓存区,单一消费者、生产者可省略此信号量
  • fill_count信号量:已使用缓存区数量
  • empty_count信号量:空闲缓存区数量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
semaphore mutex = 1
semaphore fill_count = 0
semaphore empty_count = BUFFER_SIZE

producer():
while true:
item = produce_item()
down(empty_count)
down(mutex)
put_item_into_buffer(item)
up(mutex)
up(fillcount)

consumer():
while true:
down(fill_count)
down(mutex)
item = remove_item_from_buffer()
up(mutex)
up(empty_count)
consume_item(item)

状态监控解决方案

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
item_count = 0
condition full
condition empty

add(item):
while item_count == BUFFER_SIZE:
wait(full)

item_count = item_count + 1
put_item_into_buffer(item)

if item_count == 1:
notify(empty)

remove():
while item_count == 0:
wait(empty)

item_count = item_count - 1
item = remove_item_from_buffer()

if item_count == BUFFER_SIZE - 1:
notify(full)

produer():
while true:
item = produce_item()
add(item)

consumer():
while true:
item = remove()
consume_item(item)
  • 互斥信号没有保护关键区,监控方法更好

其他

  • 协调生产者、消费者的关闭

    • 可在在队列中放置特殊的值,消费者读到时终止执行,结束 消费者线程
    • 有多个消费者时,消费者读取特殊值之后可将特殊值放回 队列中继续传递,直至关闭所有消费者

哲学家就餐问题

  • 哲学家围坐在圆桌旁,只能吃饭或者思考,每两个哲学家 之间只有一根筷子,只有同时拿到左右两根筷子才能正常吃饭
  • 实际计算机问题中,筷子视为共享资源

服务生

  • 引入服务生判断资源是否能被获取
  • 引入服务生,哲学家必须经过其允许才能拿起筷子

  • 服务生知道有哪些筷子正在被使用,能够判断是否会死锁

资源分级

  • 为资源(筷子)分配偏序关系
    • 约定所有资源都按此偏序获取、相反顺序释放
    • 且保证不会有无关资源同时被同一工作获取
  • 哲学家只能拿左右侧筷子:不会有无关资源被同一工作获取

  • 将筷子按顺序编号:资源分配偏序关系

    • 哲学家只能先拿左右筷子中编号较小者
    • 哲学家需要先放下筷子中编号较大者
  • 最实用的解法:为锁指定常量分级,强制获取顺序的顺序
  • 策略不总是实用的,尤其是所需资源列表事先不知道,可能需要 先释放已获取资源、获取低编号资源、重新获取资源,效率不高

Chandy/Misra解法

  • 标记资源,保留未使用资源、交出已使用资源,初始所以资源 已使用
  • 每根筷子分为干净、脏,最初所有筷子都脏

  • 对每对竞争同一筷子的哲学家,新拿筷子给编号较低者

  • 当哲学家需要某筷子时

    • 向其竞争对手发送请求
    • 拥有筷子的哲学家收到请求
      • 若筷子干净则保留
      • 否则擦干净交出
  • 哲学家吃完东西后,筷子变脏

    • 若有哲学家之前请求过该筷子,擦干净交出
  • 有很大并行性,适合任意大问题

读者-写者问题

  • 多线程同时访问共享内存地址,线程写入时其他线程不能读取、 写入,多个线程可以同时读取
  • 一般使用readers-writer lock解决问题

读者优先

  • 若共享内存被读取,其他读者可以立即、同时读取
  • 若一直有读者开始读取,则写者会一直被插队、无法修改

写者优先

  • 如果写者在排队,应该尽快写入共享内存
  • 若一直有写者准备写入,则读者会一直被插队、无法读取

限定时间

  • 共享内存区的锁定权要在限定时间内结束
  • 能避免读者、写者一直排队

熟睡的理发师问题

  • 理发店只有一名理发师、一张理发时坐的椅子、若干普通椅子 供顾客等待
    • 没有顾客时理发师在理发椅子上睡觉,顾客到达后离开、 或者叫醒理发师
    • 有顾客时,理发师为别人立法,顾客达到后若有空闲普通 椅子则坐下休息、否则离开
    • 理完发后,任选顾客开始理发
  • 理发师等待顾客、顾客等待理发师,造成死锁
  • 有顾客不按顺序等待,让某些顾客永远不能理发

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
semaphore customer = 0
semaphore barber = 0
semaphore mutex = 1
empty_chairs = BUFFER_SIZE

barber():
while true:
if empty_chairs == BUFFER_SIZE:
sleep()

down(mutex)
item = get_customer_from_chairs()
empty_chairs += 1
up(mutex)

down(barber)
cut_hair(item)
up(barber)

customer():
while true:
down(mutex)
if empty_chairs > 0:
empty_chairs -= 1

else:
wait() or leave()

up(mutex)

三个烟鬼问题

  • 香烟需要:烟草、卷烟纸、火柴,三个烟鬼分别有无限各一种, 不吸烟协调人会随机安排两个烟鬼各拿出一份材料放在桌上, 另外一个烟鬼拿到材料卷烟、抽
    • 桌上空了后,协调人就随机要求烟鬼拿出材料
    • 烟鬼只会在抽完手中烟后才会卷另一只
    • 若烟草、卷烟纸在桌上,有火柴的烟鬼在吸烟,直到该烟鬼 吸完烟拿走桌上材料才会继续
  • 问题模拟程序中4种角色,展示信标方法作用有限