Daemons and Services Programming Guide
一、关于Daemons和Services
大多数不用用户交互的任务都会通过一个运行在后台的进程高效处理。你可以将daemon或service用到:
注意:该文档以前的标题是“系统启动编程指南”。
1、概述
该文档指导那些需要开发daemons和其他底层系统服务的人员,编写相关代码并将其集成到启动继承。它也为系统管理员管理启动进程,提供了一些有用的信息。
数据介绍;“服务”和“守护进程”在不同上下文中含义不同,并随时间及使用人群而不同。这里,我们将服务定义为能够以某种方式,如通过注册一个全局的热键或通过网络通信来支持完整GUI应用的一个后台进程。守护进程是指所有其它类型的后台进程,特别是指那些没有任何用户接口的进程。
1.1 设计后台进程
Mac OS X提供了多种后台进程类型,它们各具特点,设计应对不同情况。同样,其它进程也有很多方式来与后台进程通信。选择合适的后台进程的设计是重要的第一步。
创建后台进程时两个重要的设计考虑是:它如何被运行,以及其它进程将如何与之通信。这两个考虑是相互影响的:不同的后台进程类型有不同的通信方式。
相关章节:“设计Daemon和Services”。
1.2 实现你的后台进程
做好决定之后,你就要准备开始编写代码了。这些章节将通过创建特定类型的后台任务的过程来指导你。
相关章节:“添加Login Items”、“创建XPC服务”、“创建Launch Deamons和Agents”
1.3 记录错误和告警
日志记录是后台进程运行记录异常行为和错误条件的的有效手段。Mac OS X提供的日志记录机制的使用在调试和解决终端用户问题时很有蚌埠。
相关章节:‘记录错误和告警“
1.4 按时间表运行任务
尽管推荐后台任务应该按组启动,在某些情况下,按时间表运行任务却是最有效的解决方案。
相关章节:”调度定时任务“
2、参考
Daemons and Agents提供了关于实现daemon和agent的额外一些信息。
Kernel Programming Guide和Kernel Extension Programming Topics描述了如何编写内核扩展和其他内核级别的后台进程。
Networking Overview 描述了用于通过发送和接受数据的API。
二、设计Daemons和Services
创建后台应用程序时最重要的两件事是,它将如何运行和其他进程如何与之通信。这两件事是交织在一起的:不同类型的后台进程有不同形式的通信。
1、后台进程的类型
Mac OS X共有四种不同类型的后台进程,他们之间的不同点在表1-1中进行了总结。选择时需要考虑以下因素:
表1-1
类型 | 由launchd管理? | 运行在何种上下文中? | 能否显示UI |
Login item | No* | User | Yes |
XPC服务 | Yes | User | No |
Launch Daemon | Yes | System | No |
Lauch Agent | Yes | User | Not recommented |
关于系统和用户上下文,参见Multiple User Environment Programming Topics.
(1)Login item
Login items是在用户登录时启动的,一直运行到用户登出或手动将其关闭为止。他们的主要用途是让用户自动打开经常使用的程序,但也可以被程序开发人员使用。例如,一个login item可以用来显示额外的菜单或注册一个全局热键。
例如,很多应用程序都使用login item来侦听全局热键,以及显示一个最小的UI让用户输入新的任务。Login items也经常用来显示用户接口项,如floating clock或timer,或者在菜单栏中显示图标等。
另外一个例子是日历应用有一个作为助手的应用程序被作为login item启动。该助手应用运行在后台,并在日历上相关的约会提醒时间快到时,启动主GUI应用程序。
更多关于如何创建login item的信息,参见”添加Login Items“。
(2)XPC服务
XPC服务由launchd管理,并为单个应用提供服务。它们通常用来将应用程序分割成更小的部分,如果进程奔溃了,可以通过限制其影响范围来改善可靠性,如果进程被黑客攻陷,则可以通过限制其影响范围而改善其安全性。
对于传统的单执行应用程序来说,如果程序的任何部分出了问题,整个应用都会终止。通过将应用程序构造成主进程和服务两部分,就可以减轻因服务崩溃带来的影响,用户可以继续工作,而奔溃的服务也可以被重启。例如,email应用程序可以通过一个XPC服务来处理与mail服务器的通信,即使该服务崩溃了,导致与服务器的通信临时中断,这也不会影响程序其他功能的使用。
沙盒可以让你指定在程序正常运行期间为其指定你希望它执行的事情。操作系统严格按你的指定执行,这就限制了因攻击而导致的危害范围。例如,文本编辑器可以需要编辑磁盘上的已经被其他用户打开的文件,但它可能不需要打开那个位置上其他任意文件,或者通过网络进行通信。
可以结合沙盒和XPC服务通过分割复杂应用、工具或daemon到更小的有良好功能定义的片中,来实现权限隔离。因为每个独立的小片的权限有限,漏洞被黑客攻击的可能性就会减小:每个小片都不可能运行全部功能。例如,一个用来组织和编辑照片的应用程序不会需要网络访问,但是,如果它允许用户上传照片到网站,该功能就应该作为带有网络访问但对文件系统访问受限(或无访问权限)的XPC服务来实现。
关于如何创建XPC服务的更多信息,参见”创建XPC服务“。
(3)Launch Daemon
Daemons在系统上下文中由launchd代替OS进行管理,这意味着它们并不知道登录到系统的用户。守护进程不能直接与用户进程打交道,它们只能响应用户进程的请求。因为不知道用户是谁,守护进程也无法访问窗体服务器,因此无法提供可视化接口或启动GUI应用。守护进程是严格的后台进程,响应底层的请求。
大多数守护进程运行在系统的系统上下文中,也就是说,他们运行在系统的最底层,并对所有用户会话开放他们的服务。这个层级的守护进程即使没有用户登录到系统时也一直在运行,因此守护进程程序对用户没有任何直接的了解。相反,守护进程必须等待用户程序联系它并发出请求。作为请求的一部分,用户程序要经常告诉守护进程如何返回结果。
更多有关如何创建launch daemon的信息,参见”创建Launch Daemons和Agents“。
(4)Launch Agent代理(Agents)由launchd管理,由launchd代替当前登录的用户(也就是在用户上下文中)运行该Agent。在同一个用户会话中,代理可以与其他进程通信,也可以在系统上下文中与系统范围的daemon通信。他们可以显示可视化接口,但不推荐这么做。
如果你的代码同时提供了特定用户和用户独立的服务,你就需要同时创建守护进程和代理。你的守护进程应该运行在系统上下文中,而代理的实例应该运行在每个用户会话中。代理可以结合守护进程来为每个用户提供服务。
更多信息参见”创建Launch守护进程和代理“
2、与daemon通信的协议
有四种机制用于daemon和它们的client进行通信:XPC、传统的Client-Server通信(包括Apple events、TCP/IP、UDP、其他socket和pipe机制)、远程过程调用(包括Mach RPC、Sun RPC、分布式对象)、内存映射(使用底层的核心图形API)。
如果daemon提供一个服务给单个应用,使用XPC最简单。给API仅在应用程序和其bundle中的一个XPC服务之间的通信有效。更多细节,参见”创建XPC服务“和”XPC服务API参考“。
在其他大多数情况下,需要使用传统的client-server通信API。基于这些API的代码似乎比RPC或内存映射设计更容易理解、调试和维护,也基于RPC的代码更容易移植到其他平台。更多细节见”CFSocket参考,UNIX Socket FAQ“,以及Apple时间编程指南。
RPC(远程过程调用)机制,如分布式对象,应该尽量避免用来在跨安全域边界之间进行通信,例如,一个用户进程与一个系统级的守护进程进行通信,这么做有安全风险。他们仅适合用在两个机场南横都有相同级别的权限的时候。更多细节见”分布式对象编程指南“。
内存映射需要复杂的管理,如果你对内存分页不当心,可能会有安全风险。你应该仅在你的代理和守护进程需要对老系统或应用有大量共享状态时再用内存映射,例如实时传递音频或视频。更多细节见”共享内存“。
3、查看当前运行的守护进程
Activity Monitor
三、Daemon的生命周期
1、启动用户环境
Mac OS X上的根进程是launchd(代替了老版本Mac OS X和传统Unix上使用的mach_init和init进程)。为了初始化系统,launchd进程以有序的方式启动系统的守护进程。就像inetd进程,launchd按需启动守护进程。以这种方式启动的守护进程可以在进程不活动和需要重启的时候关闭。当有服务请求到达时,如果守护进程未启动,launchd自动启动该守护进程来处理请求。
按需启动守护进程可以是解放内存和其他相关的资源,如果守护进程长时间空闲的话,这么做很值得。更为重要的是,这么做可以保证满足守护进程的运行时依赖,无需手动列出依赖关系。
作为系统初始化的最后一步,launchd启动loginwindow。loginwindow程序控制用户会话的很多方面,请负责登录窗口的显示和用户的认证。
更多信息见“The Early Boot Process” inKernel Programming Guide。
2、认证用户(两种情况下logwindow将绕过登录框:一是系统管理员配置用户自动登录,二是在软件安装期间,当重启后安装程序将立刻登录)
Mac OS X要求用户在访问系统之前验证自己的身份。loginwindow程序协调登录进程(作为用户输入名字和密码信息的窗口)的可视化部分和安全部分(处理用户的认证)。一旦用户通过认证,loginwindow就开始建立用户环境。
在两种关键情况下,loginwindow会绕过通常的登录提示框,直接开始用户会话。一种是当系统管理员配置了系统自动为特定用户登录。另一种是在软件安装期间,重启系统后安装程序立刻被启动。
3.配置用户会话
在用户成功认证之后,loginwindow立刻建立其用户环境并记录登录信息。作为该进程的一部分,它执行以下任务:
一旦用户会话启动并运行,loginwindow通过下列方式监视会话以及用户应用程序:
4、登出
登出、重启系统或者关闭系统的过程都是类似的。一个典型的登出/重启/关闭发生在以下情况:
5、终止进程
作为登出,重启、或者关闭的一部分,loginwindow尝试终止所有前台和后台用户进程。
你的进程应该对突然终止为用户提供最好的用户体验。参见NSProcessInfo Class Reference来了解如何采用这种技术。如果你的进程支持突然终止,它将仅被发送一个SIGKILL信号。如果你临时禁用了突然终止,就使用正常过程。
对于Cocoa应用程序,终止时通过Application Kit来部分处理的,它调用了applicationShouldTerminate:
委托方法。要中止termination过程,就要实现该方法并返回NSTerminateCancel
,否则,应用程序的termination仍然继续。
非Cocoa应用受到一个“Quit Application”的Apple事件(kAEQuitApplication),作为友好,给他们一个机会来优雅地关闭。进程将会立刻中止自己或在需要用户确认时提交一个告警对话框(例如,如果有未保存的文档)。一旦条件满足了,应用程序就会终止。如果用户决定放弃termination过程(通过点击Save对话框中的Cancel),应用程序将通过返回一个userCanceledErr错误来响应事件。
如果前台应用程序应答失败,或者在关闭自身45秒后失败,loginwindow自动放弃termination过程。safeguard进程会在多种场合保护数据,如当应用程序正在保存一个大文件到磁盘,无法在规定时间内终止。如果一个前台应用程序无响应,并且不做任何事,用户必须使用Force Quit窗口来退出。
对于后台进程,这个过程稍有不同。loginwindow程序通知进程,它将要会收到一个Quit Application的Apple事件(kAEQuitApplication
)来关闭自己。与前台进程不同,loginwindow不会等待应答。它继续通过发送SIGKILL喜好来终止任何打开的后台程序,忽略所有返回的错误。
如果系统正在被关闭或重启,它会发送一个SIGTERM信号给所有守护进程,数秒后发送SIGKILL信号。
6、初始化Logout、Restart或Shutdown
要编程实现初始化Logout、restart或shutdown序列,前台应用必须发送相关的Apple事件给loginwindow。收到事件后,loginwindow开始关闭用户会话的过程。
下面列出了Apple事件对登出、重启和关闭过程的偏好。这些事件不没有必需的参数。
kAELogOut
kAEShowRestartDialog
kAEShowShutdownDialog
除了偏好的Apple事件外,还有两个额外事件告诉loginwindow来立刻处理重启或关闭序列:
警告:如果你发送这些事件给loginwindow,用户就没有机会取消该操作,未保存的数据会丢失。
kAERestart
kAEShutDown
四、添加Login Items
有两种田间login item的方法:使用服务框里框架,使用共享文件列表。
使用服务管理框架安装的login items在系统偏好中是不可见的,并且只能由安装他们的程序删除。
使用共享文件列表安装的login items可以在系统偏好中看到。用户对他们有直接的控制。如果你使用该API,你的login item可以被用户禁用,因此任何其他与之通信的应用都需要在login item被禁用时作出合理的反馈行为。
1、使用服务管理框架增加login items
作为一个完整的应用程序bundle,应用程序可以包含一个助手应用,存储在主应用程序bundle中的Contents/Library/LoginItems目录下。在助手应用程序的bundle中的Info.plist中设置LSUIElement或LSBackgroundOnly键。
使用SMLoginItemSetEnabled函数(在Mac OS X v10.6.6及以后版本有效)来启用助手应用。它带有两个参数,一个CFStringRef包含助手应用的bundle标识符,一个Boolean指定期望的状态。传递true来立刻启动助手应用程序,并指明它将在每次用户登录时启动。传递false来终止助手应用并指明用户登录时不再启动它。如果需要的修改已经生效则函数返回true,否则,返回false。这个函数可以被用来管理任意数量的助手应用。
如果多个应用(例如,同一公司的几个应用)含有使用相同的bundle标识符的助手应用,那么只有最大bundle版本号的应用被启动。任何含有助手应用程序拷贝的引用都能够启用或禁用它。
2、使用共享文件列表添加login items
Mac OS X 10.5及后适用。参见Launch Services Reference。
3、废弃的APIs
在Mac OS X的先前版本中,可以通过发送一个苹果事件来添加login items,这可以使用CFPreferences API,并手动编辑属性列表文件。但这些方法已经废弃了。
如果你需要保持与老版本的Mac OS的兼容,一种较好的方法是使用Apple事件。更多的细节可见LoginItemsAE。使用CFPreferences API是一个可接受的选择。你不应该直接编辑任何版本Mac OS X上的属性列表文件。
创建XPC服务
XPC服务API是libSystem的一部分,为基本的进程间通信提供了一个轻量级机制,这种通信集成了Grand Central Dispatch(GCD)和launchd。
使用XPC服务的原因:
一是稳定性。
二是权限隔离。传统程序如果被人黑了,例如通过缓冲区溢出或安全漏洞方式,攻击者可以获得最高权限做任何事情。为了减轻这种后果,Mac OS X使用了沙盒技术:限制进程可以执行的操作种类。在沙盒环境中,你可以通过权限隔离进一步提高安全性:将应用切分成更小的块,每块负责一部分的应用程序行为。这就使每个切片都有一个更为严格的沙盒。其他机制也可以将程序切分成更小的块,例如NSTask盒posix_spawn(2),但它们不允许你将程序的每小块放入自己沙盒中,因此用它们来实现权限隔离。XPC服务可以继承主应用程序的沙盒,或者拥有自己的沙盒。将XPC放在自己的沙盒中可以实现权限隔离。
建议开发与launchd兼容的守护进程,launchd可以为守护进程提供更好的性能和灵活性。
有四种使用launchd启动守护进程。较好的一种方法是按需启动,还有启动连续运行的守护进程,代替inetd来启动inetd类型的守护进程,以及在规定时间间隔里启动。
launchd启动过程
1、根据/System/Library/LaunchDaemons和/Library/LaunchDaemons目录下的属性列表文件,为所有按需启动的系统级别的daemon加载参数;
2、对上述daemons要求的sockets和文件描述符进行注册;
3、启动所有需要一直运行的daemon;
4、一旦有针对特定服务的请求到达,它就启动该响应的daemon并将请求传给它;
5、当系统关闭时,它发送SIGTERM信号给由它启动的所有daemon。
对每用户的agents的启动过程与之类似。
创建launchd属性列表文件
放在/Library/LaunchDaemons、/Library/LaunchAgents目录下,类似的位置还有:/usr/local/libexec。
受launchd管理的进程的行为
1、必需的行为
提供属性列表文件;不能daemonize你的进程(包括调用daemon进程、在调用fork之后跟着exec、调用fork之后跟着exit等);全局安装的daemon和agent必须为root用户所有。
2、推荐的行为
等daomon完全初始化后再处理请求;在你的launchd配置属性列表文件中注册要用到的socket和文件描述符;如果daemon需要广播一个socket,就和launchd一起签入作为daemon初始化的一部分;在签入期间,从launchd获取字典,抽出并存储起内容,然后丢弃该字典;提供一个用于捕获SIGTERM的句柄。
内核扩展
如果daemon在执行前需要加载某些内核扩展,有两种选择:你亲自加载,或等待它被加载。
日志
可以使用ASL或这syslog,前者是apple的,后者是Unix和Linux的可跨平台。
定时任务的调度
有两种运行定时表上后台任务的方式:launchd job和cron job。
1、使用launchd调度定时任务
每个launchd任务都由一个单独的文件描述,这样以来管理launchd定时任务就可以通过添加或删除一个文件来实现。
2、使用cron
不是推荐的方法。
启动项(即将被淘汰)
启动项是一个特殊的包,在boot进程的最后阶段或其他预定的时间执行起代码。典型情况包含一个shell脚本,或者其他可执行文件。
/System/Library/StartupItems和/Library/StartupItems
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
在BSD编程中,用户ID(UIDs)关联着进程(通常是进程的所有者)。在传统BSD系统中,这些UID控制着进程的功能,你或多或少地可以认为UID匹配的两个进程有相同的功能。但是在MAC OS X中并非如此,因为还有其他的进程上下文元素在很大程度上区分着他们的功能。例如,如果一个Daemon的UID被设置为是一个登录终端的用户,那么它与使用该用户启动的应用程序是不同的。下述部分将描述进程上下文的一些元素,以及他们如何影响后台程序。
UID
进程UID(effective(EUID)、real(RUID)、save(SUID)UID)是最为知名的进程上下文元素。这些UID控制着进程的多种功能,多为系统的BSD部分(文件系统、网络、BSD进程等)。例如,打开一个进程的能力受控于它的EUID,而该进程向另一个进程发信号的能力受控于它的EUID以及目标的EUID和RUID。
(在BSD中,RUID是创建该进程用户/进程,只有在该运行程序的RUID=0时它才能被修改。EUID用于评估执行特定行为的进程权限,如果EUID!=0,那么EUID可以被修改为RUID或SUID,如果EUID=0,它可以被任意修改。如果一个启动的二进制映像文件设置了Set-UID比特位,那么SUID就是文件所有所有者的UID。否则,SUID就是RUID。正常的程序,例如ls、cat、echo由正常用户运行,并处于这些用户UID之下。允许用户对受保护的数据受控访问的特定程序可以有Set-UID比特位来让程序运行在特权UID之下。)
Mach Bootstrap基础
很多Mac子系统通过使用一个中心服务交换Mach消息来工作。这些子系统要想工作,必须能够找到该服务,这个工作通常都是由Mach的bootstrap服务来实现,它可以通过进程通过名字来找服务。每个进程都从其父进程那里继承了对bootstrap的引用。要了解其工作原理,可以运行BootstrapDump程序(示例代码项目“BootstrapDump”)。它列出了对特定进程可用的服务。
Bootstrap名字空间
每个登录会话都有其自己的bootstrap名字空间,所有运行在该会话中的进程继承了该引用。因此,当Dock注册了它的服务后,这个注册就会进入登录会话名字空间。该会话里的任何进程都会从相同的名字空间获得引用,因此可以看到Dock服务。其他登录会话中的进程引用不同的名字空间,从而避免混淆。值得一提的是每会话GUI和非GUI名字空间之间的区别。当用户通过GUI登录时,每会话GUI的bootstrap名字空间由GUI基础架构工具(loginwindow和window服务器)创建。当用户通过sshd登录时就会创建每会话bootstrap名字空间。
一个会话中的某个进程注册了服务后,它仅能在该会话空间中显示,因此也只能被该会话中的其他进程看见。但是,注册到全局名字空间的服务可以被所有进程发现。
实际上,这意味着:
需要记住的原则:
Windows Server
Windows Server是联系所有应用程序的一个单点,它是GUI框架(AppKit和HIToolbox)和其他服务,如进程管理器的实现中心。windows server不仅仅是一个管理窗口,即使是没有用户接口的应用程序(如后台应用)都要依赖windows server。windows server提供的大多数服务都使用Mach消息实现。因此,要可靠使用Windows server,必须继承指向有效每用户bootstrap名字空间的引用。这是上述规则所希望的结果。
连接权限
如果EUID为0或与该console用户的UID匹配,该进程就只能使用全局windows serer服务。
Daemon IPC建议
大多数daemon使用IPC在daemon和它的client之间进行通信。编写daemon首先要设计IPC通信机制。这里的建议对编写要与处在不同bootstrap名字空间里的client通信的daemon程序很重要。如果所有进程都运行在同一个bootstrap名字空间,你可以忽略这些建议。例如,如果编写一个agent并且所有client都是相同登录会话中的GUI应用程序,那么使用Apple事件来通信是最好的选择。
Mach API代表针对内核的最底层接口。正因如此,它们最有可能随着系统的发展而改变。Apple一直建议第三方开发人员避免使用它们。这以原则同样适用于daemon和agent。但是,对于daemon和agent来说,Mach API甚至更为麻烦,因为它们需要考虑bootstrap名字空间。只要可能,就应该避免使用Mach API。例如,使用CFMessagePort来代替通过Mach API发送和接收Mach消息。这是个很好的建议,但是,在编写daemon和agnet时,仍然会遇到bootstrap名字空间的问题。CFMessagePortCreateLocal将服务注册为bootstrap服务,CFMessagePortCreateRemote使用bootstrap服务来通过名字查找注册过的服务。因此,即使你避免了直接使用Mach消息,但如果你使用的API寒暑建立在Mach消息之上,你仍然需要考虑bootstrap名字空间。
最可能坑爹的上层API是:
总之,完全避免Mach消息就会变得容易些。Mac OS X提供了很多其他IPC机制,例如UNIX域套接字。
Unix Domain Sockets
Unix域套接字有点象TCP/IP套接字,除了其通信实在计算机本地之外。可以使用用于TCP/IP套接字相同BSD套接字API来访问UNIX域套接字。主要不同点在于地址格式。对于TCPIP套接字,地址结构(用于bind、connect等)是(struct sockaddr_in),它包含了一个IP地址和一个端口号。对于Unix域套接字而言,地址结构是(struct sockaddr_un),它包含了一个路径。当服务器绑定到一个UNIX域套接字时,系统将创建一个文件系统对象来表示该套接字。例如,PPP守护进程的UNIX域套接字是/var/run/pppconfd。当你使用ls -l命令查看时,你会发现力表的第一个字符时‘s’,表明该对象时一个套接字。
服务器一旦运行,client就可以通过传递这一路径给connect调用来连接它。一旦连接建立,通信处理就如同TCPIP一样。
使用UNIX域套接字的的好处
当实现一个daemon时,UNIX域套接字API在IPC机制上提供了很多便利。
在使用UNIX域套接字时,要记住以下几点:
编码建议
对编写后台程序程序的一些建议。
按需启动
为了最小化系统资源的使用,最好按需启动后台程序(daemon或agent)。Mac OS X提供了多种方式达成这个目的:
你可以根据启发规则插入多种不同的上层服务器。
Daemonization
编写daemon时,你可能需要在启动时刻daemonize它,是否需要daemonization取决于你如何启动。
startup item: must
mach_init daemon: should not
inetd daemon: must not
xinetd deamon: must not
launchd daemon: must not
daemon的日志
Apple系统日志(ASL)——在10.4后引入。
Syslog——BSD日志工具
Console LOG
小贴士:
启动Daemon
写daemon时,可能会遇到启动问题:1、不要强迫用户重启;2、如果你从你的配置应用程序(安装器)直接启动daemon,它会集成错误的上下文。
解决办法:
1、如果你的daemon仅使用daemon安全矿建,且没有直接或简洁使用或注册Mach消息服务,你应该能够直接启动它。它可能会运行在一个错误的上下文中,但这不会引起任何问题。
2、如果你有一个launchd deemon,你可以通过launchctl启动它。launchctl通过发送消息给launchd来工作,要求它替你启动该daemon,因此你的daemon时launchd的一个child,并继承了正确的上下文;
重要:要使其正常工作,你必须以root身份运行launchctl(EUID和RUID都必须是0)。
3、除了上述两条外,你可以使用StartupItemContext工具在全局bootstrap名字空间中启动daemon。
注意:StartupItemContext在10.3被引入。
++++++++++++++++++++++++++++++++++++++++++++++++++
典型daemon进行如下操作: