2.进程管理

创建进程

使用fork函数创建进程
int pid = fork();
在执行此函数后,即从当前进程开了一个新的子进程。
pid=0表示当前是新进程,不为0代表当前为主进程,pid即子进程的编号。

创建线程

  • 线程复制执行二进制指令
  • 多进程缺点: 创建进程占用资源多; 进程间通信需拷贝内存, 不能共享
  • 线程相关操作
    • pthread_exit(A), A 是线程退出的返回值
    • pthread_attr_t 线程属性, 用辅助函数初始化并设置值; 用完需要销毁
    • pthread_create 创建线程, 四个参数(线程对象, 属性, 运行函数, 运行参数)
    • pthread_join 获取线程退出返回值, 多线程依赖 libpthread.so
    • 一个线程退出, 会发送信号给 其他所有同进程的线程
  • 线程中有三类数据
    • 线程栈本地数据, 栈大小默认 8MB; 线程栈之间有保护间隔, 若误入会引发段错误
    • 进程共享的全局数据
    • 线程级别的全局变量(线程私有数据, pthread_key_create(key, destructer)); key 所有线程都可以访问, 可填入各自的值(同名不同值的全局变量)
  • 数据保护
    • Mutex(互斥), 初始化; lock(没抢到则阻塞)/trylock(没抢到则返回错误码); unlock; destroy
    • 条件变量(通知), 收到通知, 还是要抢锁(由 wait 函数执行); 因此条件变量与互斥锁配合使用
    • 互斥锁所谓条件变量的参数, wait 函数会自动解锁/加锁
    • broadcast(通知); destroy


      image.png

      image.png

      image.png

进程数据结构

image.png
  • 内核中进程, 线程统一为任务, 由 taks_struct 表示
  • 通过链表串起 task_struct
  • task_struct 中包含: 任务ID; 任务状态; 信号处理相关字段; 调度相关字段; 亲缘关系; 权限相关; 运行统计; 内存管理; 文件与文件系统; 内核栈;
  • 任务 ID; 包含 pid, tgid 和 *group_leader
    • pid(process id, 线程的id); tgid(thread group id, 所属进程[主线程]的id); group_leader 指向 tgid 的结构体
    • 通过对比 pid 和 tgid 可判断是进程还是线程
  • 信号处理, 包含阻塞暂不处理; 等待处理; 正在处理的信号
    • 信号处理函数默认使用用户态的函数栈, 也可以开辟新的栈专门用于信号处理, 由 sas_ss_xxx 指定
    • 通过 pending/shared_pending 区分进程和线程的信号
  • 任务状态; 包含 state; exit_state; flags
    • 准备运行状态 TASK_RUNNING
    • 睡眠状态:可中断; 不可中断; 可杀
      • 可中断 TASK_INTERRUPTIBLE, 收到信号要被唤醒
      • 不可中断 TASK_UNINTERRUPTIBLE, 收到信号不会被唤醒, 不能被kill, 只能重启
      • 可杀 TASK_KILLABLE, 可以响应致命信号, 由不可中断与 TASK_WAKEKILL 组合
    • 停止状态 TASK_STOPPED, 由信号 SIGSTOP, SIGTTIN, SIGTSTP 与 SIGTTOU 触发进入
    • 调试跟踪 TASK_TRACED, 被 debugger 等进程监视时进入
    • 结束状态(包含 exit_state)
      • EXIT_ZOMBIE, 父进程还没有 wait()
      • EXIT_DEAD, 最终状态
    • flags, 例如 PF_VCPU 表示运行在虚拟 CPU 上; PF_FORKNOEXEC _do_fork 函数里设置, exec 函数中清除
  • 进程调度; 包含 是否在运行队列; 优先级; 调度策略; 可以使用那些 CPU 等信息.
  • 运行统计信息, 包含用户/内核态运行时间; 上/下文切换次数; 启动时间等;
  • 进程亲缘关系
    • 拥有同一父进程的所有进程具有兄弟关系
    • 包含: 指向 parent; 指向 real_parent; 子进程双向链表头结点; 兄弟进程双向链表头结点
    • parent 指向的父进程接收进程结束信号
    • real_parent 和 parent 通常一样; 但在 bash 中用 GDB 调试程序时, GDB 是 real_parent, bash 是 parent
  • 进程权限, 包含 real_cred 指针(谁能操作我); cred 指针(我能操作谁)
    • cred 结构体中标明多组用户和用户组 id
    • uid/gid(哪个用户的进程启动我)
    • euid/egid(按照哪个用户审核权限, 操作消息队列, 共享内存等)
    • fsuid/fsgid(文件操作时审核)
    • 这三组 id 一般一样
    • 通过 chmod u+s program, 给程序设置 set-user-id 标识位, 运行时程序将进程 euid/fsuid 改为程序文件所有者 id
    • suid/sgid 可以用来保存 id, 进程可以通过 setuid 更改 uid
    • capability 机制, 以细粒度赋予普通用户部分高权限 (capability.h 列出了权限)
      • cap_permitted 表示进程的权限
      • cap_effective 实际起作用的权限, cap_permitted 范围可大于 cap_effective
      • cap_inheritable 若权限可被继承, 在 exec 执行时继承的权限集合, 并加入 cap_permitted 中(但非 root 用户不会保留 cap_inheritable 集合)
      • cap_bset 所有进程保留的权限(限制只用一次的功能)
      • cap_ambient exec 时, 并入 cap_permitted 和 cap_effective 中
  • 内存管理: mm_struct
  • 文件与文件系统: 打开的文件, 文件系统相关数据结构
image.png
  • 用户态/内核态切换执行如何串起来
  • 用户态函数栈; 通过 JMP + 参数 + 返回地址 调用函数
    • 栈内存空间从高到低增长
    • 32位栈结构: 栈帧包含 前一个帧的 EBP + 局部变量 + N个参数 + 返回地址
      • ESP: 栈顶指针; EBP: 栈基址(栈帧最底部, 局部变量起始)
      • 返回值保存在 EAX 中
    • 64位栈结构: 结构类似
      • rax 保存返回结果; rsp 栈顶指针; rbp 栈基指针
      • 参数传递时, 前 6个放寄存器中(再由被调用函数 push 进自己的栈, 用以寻址), 参数超过 6个压入栈中
  • 内核栈结构:
    • Linux 为每个 task 分配了内核栈, 32位(8K), 64位(16K)
    • 栈结构: [预留8字节 +] pt_regs + 内核栈 + 头部 thread_info
    • thread_info 是 task_struct 的补充, 存储于体系结构有关的内容
    • pt_regs 用以保存用户运行上下文, 通过 push 寄存器到栈中保存
    • 通过 task_struct 找到内核栈
      • 直接由 task_struct 内的 stack 直接得到指向 thread_info 的指针
    • 通过内核栈找到 task_struct
      • 32位 直接由 thread_info 中的指针得到
      • 64位 每个 CPU 当前运行进程的 task_struct 的指针存放到 Per CPU 变量 current_task 中; 可调用 this_cpu_read_stable 进行读取

进程调度

image.png

调度策略与调度类

  • 进程包括两类: 实时进程(优先级高),普通进程;实时进程(0-99); 普通进程(100-139)
  • 两种进程调度策略不同: task_struct->policy 指明采用哪种调度策略(有6种策略)
  • 实时调度策略, 高优先级可抢占低优先级进程
    • FIFO: 相同优先级进程先来先得
    • RR: 轮流调度策略, 采用时间片轮流调度相同优先级进程
    • Deadline: 在调度时, 选择 deadline 最近的进程
  • 普通调度策略
    • normal: 普通进程
    • batch: 后台进程, 可以降低优先级
    • idle: 空闲时才运行
  • 调度类: task_struct 中 * sched_class 指向封装了调度策略执行逻辑的类(有5种)
    • stop: 优先级最高. 将中断其他所有进程, 且不能被打断
    • dl: 实现 deadline 调度策略
    • rt: RR 或 FIFO, 具体策略由 task_struct->policy 指定
    • fair: 普通进程调度
    • idle: 空闲进程调度
  • 普通进程的 fair 完全公平调度算法 CFS(Linux 实现)
    • 记录进程运行时间( vruntime 虚拟运行时间)
    • 优先调度 vruntime 小的进程
    • 按照比例累计 vruntime, 使之考虑进优先级关系
  • 调度队列和调度实体
    • CFS 中需要对 vruntime 排序找最小, 不断查询更新, 因此利用红黑树实现调度队列
    • task_struct 中有 实时, deadline 和 cfs 三个调度实体, cfs 调度实体即红黑树节点
    • 每个 CPU 都有 rq 结构体, 里面有 dl_rq, rt_rq 和 cfs_rq 三个调度队列以及其他信息; 队列描述该 CPU 所运行的所有进程
    • 先在 rt_rq 中找进程运行, 若没有再到 cfs_rq 中找; cfs_rq 中 rb_root 指向红黑树根节点, rb_leftmost指向最左节点
  • 调度类如何工作
    • 调度类中有一个成员指向下一个调度类(按优先级顺序串起来)
    • 找下一个运行任务时, 按 stop-dl-rt-fair-idle 依次调用调度类, 不同调度类操作不同调度队列

进程抢占

  • 抢占式调度
  • 两种情况: 执行太久, 需切换到另一进程; 另一个高优先级进程被唤醒
    • 执行太久: 由时钟中断触发检测, 中断处理调用 scheduler_tick
      • 取当前进程 task_struct->task_tick_fair()->取 sched_entity cfs_rq 调用 entity_tick()
      • entity_tick() 调用 update_curr 更新当前进程 vruntime, 调用 check_preempt_tick 检测是否需要被抢占
      • check_preempt_tick 中计算 ideal_runtime(一个调度周期中应该运行的实际时间), 若进程本次调度运行时间 > ideal_runtime, 则应该被抢占
      • 要被抢占, 则调用 resched_curr, 设置 TIF_NEED_RESCHED, 将其标记为应被抢占进程(因为要等待当前进程运行 __schedule)
    • 另一个高优先级进程被唤醒: 当 I/O 完成, 进程被唤醒, 若优先级高于当前进程则触发抢占
      • try_to_wake_up()->ttwu_queue() 将唤醒任务加入队列 调用 ttwu_do_activate 激活任务
      • 调用 tt_do_wakeup()->check_preempt_curr() 检查是否应该抢占, 若需抢占则标记
  • 抢占时机: 让进程调用 __schedule, 分为用户态和内核态
    • 用户态进程
      • 时机-1: 从系统调用中返回, 返回过程中会调用 exit_to_usermode_loop, 检查 _TIF_NEED_RESCHED, 若打了标记, 则调用 schedule()
      • 时机-2: 从中断中返回, 中断返回分为返回用户态和内核态(汇编代码: arch/x86/entry/entry_64.S), 返回用户态过程中会调用 exit_to_usermode_loop()->shcedule()
    • 内核态进程
      • 时机-1: 发生在 preempt_enable() 中, 内核态进程有的操作不能被中断, 会调用 preempt_disable(), 在开启时(调用 preempt_enable) 时是一个抢占时机, 会调用 preempt_count_dec_and_test(), 检测 preempt_count 和标记, 若可抢占则最终调用 __schedule
      • 时机-2: 发生在中断返回, 也会调用 __schedule
        image.png

进程创建

  • fork -> sys_call_table 转换为 sys_fork()->_do_fork
  • 创建进程做两件事: 复制初始化 task_struct; 唤醒新进程
  • 复制并初始化 task_struct, copy_process()
    • dup_task_struct: 分配 task_struct 结构体; 创建内核栈, 赋给* stack; 复制 task_struct, 设置 thread_info;
    • copy_creds: 分配 cred 结构体并复制, p->cred = p->real_cred = get_cred(new)
    • 初始化运行时统计量
    • sched_fork 调度相关结构体: 分配并初始化 sched_entity; state = TASK_NEW; 设置优先级和调度类; task_fork_fair()->update_curr 更新当前进程运行统计量, 将当前进程 vruntime 赋给子进程, 通过 sysctl_sched_child_runs_first 设置是否让子进程抢占, 若是则将其 sched_entity 放前头, 并调用 resched_curr 做被抢占标记.
    • 初始化文件和文件系统变量
      • copy_files: 复制进程打开的文件信息, 用 files_struct 维护;
      • copy_fs: 复制进程目录信息, 包括根目录/根文件系统; pwd 等, 用 fs_struct 维护
    • 初始化信号相关内容: 复制信号和处理函数
    • 复制内存空间: 分配并复制 mm_struct; 复制内存映射信息
    • 分配 pid
  • 唤醒新进程 wake_up_new_task()
    • state = TASK_RUNNING; activate 用调度类将当前子进程入队列
    • 其中 enqueue_entiry 中会调用 update_curr 更新运行统计量, 再加入队列
    • 调用 check_preempt_curr 看是否能抢占, 若 task_fork_fair 中已设置 sysctl_sched_child_runs_first, 直接返回, 否则进一步比较并调用 resched_curr 做抢占标记
    • 若父进程被标记会被抢占, 则系统调用 fork 返回过程会调度子进程
image.png

线程创建

  • 线程的创建
  • 线程是由内核态和用户态合作完成的, pthread_create 是 Glibc 库的一个函数
  • pthread_create 中
  1. 设置线程属性参数, 如线程栈大小
  2. 创建用户态维护线程的结构, pthread
  3. 创建线程栈 allocate_stack
    • 取栈的大小, 在栈末尾加 guardsize
    • 在进程堆中创建线程栈(先尝试调用 get_cached_stack 从缓存回收的线程栈中取用)
    • 若无缓存线程栈, 调用 __mmap 创建
    • 将 pthread 指向栈空间中
    • 计算 guard 内存位置, 并设置保护
    • 填充 pthread 内容, 其中 specific 存放属于线程的全局变量
    • 线程栈放入 stack_used 链表中(另外 stack_cache 链表记录回收缓存的线程栈)
  4. 设置运行函数, 参数到 pthread 中
  5. 调用 create_thread 创建线程
    • 设置 clone_flags 标志位, 调用 __clone
    • clone 系统调用返回时, 应该要返回到新线程上下文中, 因此 __clone 将参数和指令位置压入栈中, 返回时从该函数开始执行
  6. 内核调用 __do_fork
    • 在 copy_process 复制 task_struct 过程中, 五大数据结构不复制, 直接引用进程的
    • 亲缘关系设置: group_leader 和 tgid 是当前进程; real_parent 与当前进程一样
    • 信号处理: 数据结构共享, 处理一样
  7. 返回用户态, 先运行 start_thread 同样函数
    • 在 start_thread 中调用用户的函数, 运行完释放相关数据
    • 如果是最后一个线程直接退出
    • 或调用 __free_tcb 释放 pthread 以及线程栈, 从 stack_used 移到 stack_cache 中
image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容