深入解析Mac OS X & iOS 操作系统 学习笔记(十四)

BSD 层

Mach只是一个微内核。尽管Mach的部分应用程序接口(API)也暴露给了用户态,但是开发者主要使用的还是更为流行的POSIX API,而这一套API 是通过Mach之上的BSD层实现的。

BSD 简介

在BSD出现在XNU 中之前,Mach 就和 BSD紧密结合在一起了。Mach 的陷阱和服务不足以支撑起完整的操作系统,而且Mach 缺少一些像文件系统这样最基本的服务。在Mach 提供的这些原语之上还需要建立一个层次提供像文件、设备、用户和组等重要抽象。Mach 最早选择的这个层次就是BSD,而且延续着XNU中了。

进程和线程

Mach调度中提到了任务和线程这两个原语。BSD也采用了这两个原语,并且组织成看UNIX世界中著名的进程和线程的概念。

BSD 进程结构

BSD 的进程可以唯一地映射到Mach任务,但是包含的信息比Mach任务提供的基本调度和统计信息要丰富。BSD 进程包含了文件描述符和信号处理程序的数据。进程还支持复杂的谱系,将进程和其父进程、兄弟进程和子进程连接起来。BSD 在struct proc 中维护了进程的很多特性,struct porc是一个庞然大物,拥有许多字段,因此需要多个锁来保护不同的字段,以及字段参与的列表。进程锁(PL)保护整个数据结构,还有一个线程自旋锁(PSL)、一个文件描述符锁(PFDL)以及其他保护进程组合兄弟进程的锁。

进程列表和进程组

XNU在struct proclist 变量中维护多个进程,这个变量就是struct proc 的链表。还一个遍历这些列表的特殊迭代器函数。

线程

进程就是容器,二进制代码的执行单元是线程。 BSD 的线程对象定义在struct uthread 结构体中。同样也是一个非常庞大的数据结构。用户态的线程始于对pthread_create的调用。主要工作是由bsdthread_create 系统调用完成的。bsdthread_create( ) 只不过是对Mach线程创建的复杂包装。真正的线程创建是由底层的Mach层完成的。bsdthread_create( ) 负责的工作是设置线程栈,设置线程状态以及设置自定义调度参数等。

BSD 线程对应到 Mach 任务

底层的Mach 微内核才是真正主要实现进程和线程结构原语的实体。每一个Mach 任务都包含一个bsd_info 指针指向其对应的BSD proc 数据结构;类似地,Mach 线程也包含一个uthread 字段指向对应的uthread 结构体。这些指针都是void类型的,因此Mach函数不需要具体的BSD数据结构。类似地,BSD进程也通过task字段指针指回其对应的任务(同样也是void*);BSD线程(uthread)也是通过vc_thread字段指回对应的Mach 线程,这个vc_thread字段是uthread的uu_context字段中的子字段。在Mach这边,Mach 调用task_for_pid( ) 用于将BSD的PID转换为底层Mach任务的端口。在XNU中,所有的内核线程都是Mach线程,没有对应的BSD进程。也就是说,这些线程的uthread指针都为NULL,并且都包含在kernel_task中。类似地,kernel_task 也没有BSD 进程标识符(处理PID 0之外)。

进程创建

用户态角度

在UNIX中,进程不能被创建出来,只能通过fork( ) 系统调用复制出来。fork( ) 只是一个特殊的系统调用,调用一次返回两次:

  • 在子进程中,fork( ) 返回0
  • 在父进程中,fork( ) 返回子进程的PID
    如果fork( ) 操作失败,fork( ) 只会在调用的进程中返回-1,并且设置对应的errno值,通常是EAGAIN或ENOMEN。
    子进程是父进程的完整复制,除了以下几个重要的例外:
  • 文件描述符,尽管数值和指向的文件都是一样的,但只是原始描述符的副本。这意味着后续对这些描述符修改的调用(例如lseek( ) 和 close( ) )只会影响创建这些描述符的进程
  • 资源限制,即getrlimit( ) 和 ulimit( ) 调用设置的值,子进程会继承资源限制,但是资源利用率为0
  • 子进程的内存镜像(从虚拟内存的角度)看上去是子进程私有的,但是事实上(从物理内存的角度看)子进程和父进程使用的是内存中相同的物理页面。虚拟内存的私有性是通过设置页面的写时复制标志位保证的,因此不论是哪个进程(子进程或者父进程)视图写入页面是都会引发页错误。在处理页错误时,内核创建页面的副本,创建同一页的另外一个物理页面副本,并且重新建立映射
内核态的角度

不管用户使用了哪一个系统调用:fork( )、vfork( )或posix_spawn( ),内核中的所有路径都会归一到同一个底层的实现,那就是fork( )。这个调用的行为取决于第三个参数:kind,每一个系统调用传入不同的值,如下表
int fork1(proct parent_proc, thread_t *child_threadp, int kind);

kind值 创建的进程 地址空间
PROC_CREATE_FORK 完整 复制(写时复制)
PROC_CREATE_VFORK 部分 新创建的
PROC_CREATE_SPAWN 完整 延迟创建(不可用)

fork1( ) 最终复制创建新的进程,这个调用会创建这个进程对应的新Mach 任务。尽管三个不同的系统调用最终都会汇聚到这个调用。如果是vforl调用,fork1( )则调用forkpric( ),其他情况下,则调用cloneproc( )。后者是对forkproc( ) 的封装,但是完成更多任务,关于fork1( ) 分支多种创建的调用如下图:

fork1( ) 分支到多种进程创建的调用.png
forkproc( ) 函数

不论调用来自fork( )、vfork( )还是posix_spwan( ),forkproc( ) 函数的职责都是初始化新进程的proc_t 数据结构。处理的流程如下所示:

  • 从 M_PROC Zone 中分配 proc_t 类型的 child_proc,并且调用bzero 用0填充
  • 分配子进程的统计信息数据结构(p_stats)和信号处理动作数据结构(p_sigacts)
  • 分配间隔定时器callout(p_rcall)
  • 获得子进程的PID,要处理PID越过PID_MAX(99999)的情况。将PID插入PID散列表
  • 初始化进程的其他字段。大部分字段都是通过bcopy( ) 直接从父进程复制过来,包括从父进程的p_startcopy指针(被设置为p_argslen)到p_endcopy(被设置为p_aio_totalcount)的范围。有一些字段除外
  • 通过fdcopy( ) 复制父进程的所有文件描述符
  • 通过shmfork( ) 复制父进程的System V 共享内存(#if SUSV_SHM)
  • 通过proc_limitfork( ) 复制父进程的资源限制
  • 将p_stats 中从pstat_startzero(设置为p_ru)到pstat_endzero(设置为p_stat)的范围通过bzero( ) 设置为0.将p_start(进程的启动时间)设置为当前时间
  • 如果父进程定义了信号动作(p_sigacts),则复制过来,否则将子进程的信号动作全部设置为NULL
  • 如果有的话,设置子进程的控制终端
  • 调用proc_signalstart(child_proc,0) 阻止所有信号,并调用 proc_transstart(child_proc, 0)将子进程标记为处于转换的状态
  • 初始化子进程的线程列表(p_uthlist)和异步I/O队列
  • 继承父进程的代码签名相关的标志
  • 复制父进程的工作队列信息
  • 如果父进程在登录上下文中(而且 #if CONFIG_LCTX),则通过enterctx( ) 将子进程也加入登录上下文

注意这个函数还漏了一个很重要的工作:在Mach 层次创建实际的进程和线程。在vfork( ) 中是不会完成这项任务的。只要fork( ) 和 posix_spawn( ) 才能完成。这也是为什么只有 vfork( ) 会直接调用forkproc( ),而其他情况下则是由cloneproc( )调用forkproc( ) 的原因 ,cloneproc( ) 会创建必要的 Mach 数据结构。vfork( ) 的进程没有对应的Mach 任务和线程。只要在接下来调用; execve( ) 之后才会创建任务和进程。事实上,除了有一个execve( ) 跟在后面之外,vfork( ) 没有存在的意义,因为这系统通过嘴馋的设计就是为了这个目的。子进程的task_t和thread_t完全就是父进程的task_t和thread_t,vm_map 也是如此。只有以后调用execve( ) 载入一个Mach-O 镜像才能最终真正创建一个Mach 任务和进程。

cloneproc( ) 函数

当传入的kind为PROC_CREATE_SPAWN和PROC_CREATE_FORK时才会调用cloneproc( ) 函数。由于我们关注的是“真正的”fork,而不是vfork( ) 所以这个函数处理吊椅forkproc( ) 之外还会执行其他操作。这个函数的流程如下:

  • 对parent_proc 调用forkproc( ),这个函数会返回proc_t 类型的child_proc,而child_proc 最终会成为子进程完整填充的控制块
  • 调用fork_create_child( ) 创建子进程的uthread
  • 根据父进程的P_LP64设置子进程的64为属性
  • 对parent_proc 和新生的child_proc 调用 pinserchild( )。这个函数将子进程插入父进程的p_children列表中,然后将子进程插入allproc列表,这样系统就知道这个新子进程的存在了。此外,这个函数还有一个副作用,就是清除子进程的p_listflahg中的P_LIST_INCREATE标志位。这个标志位是在forkproc( ) 中设置的,目的是为了让那个proc_ref_locked( ) 忽略子进程
加载和执行二进制文件

如果将一个进程比作是一个人体,那么在进程中执行的二进制程序就是这个人体的大脑。只是通过fork( ) 新创建出来的进程也没多大作用,除非执行镜像通过exec( ) 替换为另一个可执行程序。因此,进程创建的核心在于二进制文件的加载和执行。

进程控制和跟踪

Mach 提供了丰富的跟踪机制,其中最重要的就是DTrace。还有一个机制是ptrance( )

ptrace( )

BSD 和 其他UNIX 系统提供了一个名为ptace( ) 的一站式系统调用,这个调用支持进程跟踪和调试。其函数原型如下
int ptrace(int request, pid_t pid, caddr_t addr, int data);

策略

OS X 和 iOS 支持I/O 和 执行策略(policy)的概念。苹果在策略方面提供了以下两个系统调用:

  • **iopolicysys( ) **:用于本地设置I/O。策略的作用范围可以是当前线程也可以是这个线程。这个策略可以取值NORMAL(尽力而为)、THROTTLE(限制带宽)和PASSIVE(其他进程优先)
  • process_policy( ):允许对进程实施执行策略
进程挂起和恢复

挂起一个进程相当于停止一个进程的执行无限长时间,知道这个进程被恢复。通过信号停止进程运行的概念并不是OS X 和 iOS 中的进程挂起,XNU提供系统调用(而不是信号)来支持这种功能。这些系统调用只不过是对Mach API task_suspend( ) 和 task_resume( ) 的简单包装。而从iOS 5开始,这些系统调用就和Mach 的 defaulr_freezer以及进程的睡眠机制结合在一起了。通过这种设计,进程可以通过系统调用的方式选择性地冷冻和解冻进程,冷冻和解冻的决定权通常在iOS的加载器SpringBoard手中。

信号

Mach 已经通过异常机制提供了底层的陷阱处理。而BSD则在异常机制之上构建了信号处理机制。硬件产生的信号被Mach 层捕捉,然后转换成对应的UNIX 信号。为了维护一个统一的机制,操作系统和用户产生的信号首先被转换为Mach 异常,然后在转换为信号。

UNIX 异常处理程序

当第一个BSD进程(也是用户态进程)被bsdinit_task( ) 函数启动时,这个函数还调用了un_handle_init( ) 函数,这个函数设置了一个名为 un_hanler 的 Mach 内核线程。通过调用host_set_exception_ports( ) 函数,bsdinit_taks( ) 将所有的Mach 异常消息都重定向到ux_exception_port,这个端口由un_handler( ) 线程持有。遵循Mach消息传递的范式,PID 为1的进程(即launchd)的异常就会在进程之外由un_handler( ) 线程处理。由于所有后建的用户进程都是PID 1的后代,所以这些进程都会自动继承这个异常端口,相当于un_handler( ) 线程要负责处理系统上UNIX进程产生的每一个Mach 异常。

硬件产生的信号

硬件产生的信号始于处理器陷阱。处理器陷阱是平台相关的。ux_exception负责将显著转化为信号。为了处理机器相关的情况,un_exception 会调用machine_exception首先尝试处理机器陷阱。如果这个函数无法转换信号,un_exception则处理一般的情况。

软件产生的信号

如果信号不是由硬件产生的,那么这个信号源于两个API调用:kill( ) 或pthread_kill( )。这两个函数分别向进程和线程发送信号。

受害者的信号处理

不论是硬件残生的信号还是其他方式产生的信号,执行流程都会结束语 act_set_bsdast( )。这回导致收到信号的进程唤醒(准确的说,是这个进程中的某一个线程被唤醒),唤醒时执行流程进入ast_taken( ),ast_taken( )转而调用bsd_asr( )。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容