读书笔记---《理解Unix进程》

1、系统调用
Unix系统内核位于计算机硬件之上,它是与硬件交互的中介。
程序不可以直接访问内核,所有的通信都是通过系统调用来完成的。

系统调用为内核和用户空间搭建了桥梁。它规定了程序与计算机硬件之间所允许的一切交互。
系统调用允许用户空间程序通过内核间接地与计算机硬件进行交互。

节1:一般命令
节2:系统调用
节3:C库函数
节4:特殊文件

进程乃Unix系统的基石。所有的代码都是在进程中执行的。

2、 在系统中运行的所有进程都有一个唯一的进程标识符,成为pid。(进程皆有标识)
pid并不传达关于进程本身的任何信息,在内核眼中只是一个数字而已。

进程皆有父。Ppid
进程皆有文件描述符。(file descriptor number)进程打开的所有资源都会获得一个用于标识的唯一数字,这便是内核跟踪进程所用资源的方法。
文件描述符代表已打开的资源,并不会在无关进程之间共享,它只存在于其所属的进程之中。
(1) 所分配的文件描述符编号是尚未使用的最小的数值。
(2) 资源一旦关闭,对应的文件描述符编号就又能够使用了。
文件描述符只是用来跟踪打开的资源,已经关闭的资源是没有文件描述符的。

每个Unix进程都有三个打开的资源,它们是标准输入(STDIN)、标准输出(STDOUT)和标准错误(STDERR)。

3、 一个进程可以拥有多少个文件描述符?
进程皆有资源限制:内核为进程施加了某种资源限制。sysctl(8).
软限制(soft limit)和硬限制(hard limit)。任何进程都可以修改自身的软限制,只有超级用户才能修改硬限制。

4、 进程皆有环境。
所有进程都从其父进程处继承环境变量。环境变量是包含进程数据的键值对(key-value pairs)。
系统调用不能直接操作环境变量,不过可以用C库函数setenv 和 getenv 操作。
ENV并不是Hash?它实现了Enumerable和部分的hash api,但并非全部。
环境变量经常作为一种输入传递到命令行程序中的通用方法。

5、 进程皆有参数
所有进程都可以访问名为ARGV的特殊数组。argv, 是argument vector的缩写。参数向量或数组。
它保存了在命令行中传递给当前进程的参数。

ARGV最常见的用例大概就是将文件名传入程序。

6、 进程皆有名
日志文件使得进程可以通过向文件系统写入信息的方式来了解彼此的状态信息。(这种操作属于图文件系统层面,而非进程本身所固有的。)
类似,进程可以借助网络来打开套接字同其他进程进行通信。但这也并非运作在进程层面。
有两种运作在进程自身层面上的机制可以用来互通信息。一个是进程名称,另一个是退出码。

进程名称可以在运行的时候被修改。

7、 进程皆有退出码
进程在退出的时候都带有数字退出码,用于指明进程是否顺利结束。0代表顺利结束。
exit 被调用时,在退出之前,Ruby会调用由kernel#at_exit#所定义的全部语句块。

abort:提供了一种从错误进程中退出的通用方法。此种方式会调用at_exit处理程序。
raise: 使用一个未处理的异常。raise与之前的方法不同,不会立刻结束进程。只是抛出一个异常,该异常会沿着调用栈向上传递并可能会得到处理。此种方式仍然会调用at_exit处理程序。

8、 进程皆可fork
子进程从父进程处继承了其所占用内存中的所有内容,以及所有属于父进程的已打开的文件描述符。
子进程可以随意更改其内存内容的副本,而不会对父进程造成任何影响。

fork返回两次,在父进程中返回新建的子进程id,在子进程中返回0。
“fork炸弹”

9、 Cow(copy on write):将实际的内存复制操作推迟到了真正需要写入的时候。
也就是说,父进程和子进程实际上是在共享内存中的数据,直到它们其中的某个需要对数据进行修改,才会进行内存复制,使得两个进程保持适当的隔离。
MRI并不支持cow,为何?
MRI的垃圾收集器使用了一种“标记-清除mark-and-sweep”的算法。
当垃圾收集器被调用时,它必须对每个已经的对象进行迭代并写入信息,指出改对象是否应该被回收。
因此,在进行fork之后,首次进行垃圾收集时,写时复制带来的好处会被撤销。

Ruby企业版 (Ruby enterprises edition)是CoW友好的。

10、 进程可待
Process.wait是一个阻塞调用,该调用使得父进程一直等到它的某个子进程退出之后才继续执行。
当父进程拥有不止一个子进程,可以通过使用返回值来解决这个问题。Process.wait返回退出的那个子进程的pid。

使用Process.wait2进行通信。
wait2返回两个值(pid,status),通过退出码来编码信息,可让我们获知某个进程是如何退出的。这些状态可以用作进程间的通信。

进程间的通信既不需要文件系统,也不需要网络。

Process.waitpid 和process.waitpid2
他们的功能类似wait和wait2,不过不是等待任意的子进程退出,而是只等待由pid指定的子进程退出。pid= -1时,它等待任意的子进程。

内核会将已退出的子进程的状态信息加入队列,所以即便你在子进程退出很久之后才调用process.wait,依然可以获取它的状态信息。

Babysitting process:有一个进程fork出多个并发的子进程,这个父进程看管着这些进程,确保它们能够保持响应,并对子进程的退出作出回应。

11、 僵尸进程
内核会一直保留已经退出的子进程的状态信息,直到父进程使用process.wait请求的这些信息。如果父进程一直不发出请求,那么这些状态信息就会一直保留着。

此时不读取状态信息,就是在浪费内核资源。解决的办法是,分离这个子进程。
Process.detach(pid),它不过就是生成了一个新线程,这个线程的唯一工作就是等待由pid所指定的那个子进程退出。这确保了内核不会一直保留那些我们不需要的状态信息。

任何已经结束的进程,如果它的状态信息一直未能被读取,那么它就是一个僵尸进程。
一旦父进程读取了僵尸进程的状态信息,那么它就不复存在,也就不再消耗内核资源。

采用即发即弃(fire and forget)的方式fork出子进程,却对其状态信息不理不问,这种情况很少见。如果需要在后台执行工作,更常见的做法是采用一个专门的后台排队系统。

Process.detach并没有对应的系统调用,因为它在ruby中仅仅通过线程和process.wait来实现。


12、进程皆可获得信号
Process.wait为父进程提供了一种很好的方式来监管子进程。但它是一个阻塞调用:直到子进程结束,调用才会结束。

不是每个父进程都有闲暇一直等待自己的子进程结束。 此时的解决方案是:Unix信号。

信号投递是不可靠的。
如果你的代码正在处理CHLD信号,这时另一个子进程结束了,那么你未必能收到第二个CHLD信号。

如果只有一个已结束的子进程,而我却调用了两次Process.wait,又该如何避免阻塞整个进程?
Process.wait的第二个参数,用Process::WNOHANG,这个标志来告诉内核,如果没有子进程退出,就不需要进行阻塞。

信号是一种异步通信,当进程从内核那里接收到一个信号时,它可以执行下列某一操作:
(1) 忽略该信号
(2) 执行特定的操作
(3) 执行默认的操作

信号由内核发送,信号是由一个进程发送到另一个进程,只不过是借用内核作为中介。

信号最初的目的是用来指定终结进程的不同方式。

捕获一个信号有点像使用一个全局变量,但和全局变量不同的是,信号处理程序并没有命名空间。

进程可以在任何时候接收到信号。在实践中,信号多是由长期运行的进程使用,例如服务器和守护进程。

13、 进程皆可互通
有很多方法可以实现IPC,两种常见的方法:管道和套接字对。
管道是一个单向数据流。
管道提供的是单向通信,套接字对提供的是双向通信。
IO.pipe返回的IO对象可以看作类似于匿名文件的东西。基本上可以将其视作File来对待。你可以调用#read #write #close等等,但是#path对IO对象无效,IO对象在文件系统中并没有对应的位置。

管道也被认为是一种资源,它有自己的文件描述符以及其他的一切,因此也可以与子进程共享。

Unix套接字是一种只能用于在同一台物理主机中进行通信的套接字。它比TCP套接字快很多,非常适合用于IPC。套接字并不使用流,而是使用数据报(datagram)通信。

14、 守护进程
守护进程是在后台运行的进程,不受终端用户控制。Web服务器或数据库服务器都属于常见的守护进程,它们在后台运行响应请求。

Exit if fork  
这个返回值在父进程中为真,在子进程中为假。(父进程将会退出,已经成为孤儿进程的子进程照常运行)。此时孤儿进程的ppid是init进程

Process.setsid完成以下三件事情:
1. 该进程变成一个新会话的会话领导。
2. 该进程变成一个新进程组的组长。
3. 该进程没有终端控制。

Process.setsid会返回其新创建的会话组的id,你可以保存起来以备不时之需。

Rack创建守护进程的例子
Def daemonize_app
   If RUBY_VERSION < “1.9”
     Exit if fork
     Process.setsid
     Exit if fork
     Dir.chdir “/”  这步确保了守护进程的当前工作目录在执行过程中不会消失。避免了守护进程的启动目录出于这样或那样的问题被删除或卸载。
     STDIN.reopen “/dev/null”
     STDOUT.reopen”/dev/null” , ”a”
     STDERR.reopen”/dev/null” , “a”
   Else
     Process.daemon
   End
End


第一行代码fork出一个子进程,然后父进程退出。启动该进程的终端觉察到进程退出后,将控制返回给用户,但是之前fork出的子进程仍然拥有从父进程中继承而来的组id和会话id。此时这个衍生进程即非会话领导,也非组长。

但终端与衍生进程之间仍有牵连,如果它发出信号到衍生进程的会话组,这个信号仍会被接收到,但是我们想要的是完全脱离终端。

Process.setsid会使衍生进程成为一个新进程组和新会话组的组长兼领导。注意,如果在某个已经是进程组组长的进程中调用process.setsid,则会失败,它只能从子进程中调用。

已经成为进程组和会话组组长的衍生进程再次进行衍生,然后退出。这个新衍生出的进程不再是进程组的组长,也不是会话领导。由于之前的会话领导没有控制终端,并且此进程也不是会话领导,因此这个进程绝不会有控制终端。终端只能够分配给会话领导。

进程组和会话组
进程组:每个进程都属于某个组,每个组都有唯一的整数id。进程组是一个相关进程的集合,通常是父进程和子进程。进程组id和进程组组长的pid相同。进程组组长是终端命令的“发起”进程。
终端接收信号,并将其转发给前台进程组中的所有进程。

会话组:是进程组的集合,
git log | grep shipped | less 
每个命令都有自己的进程组。这是因为每个命令都可以创建子进程,但是这些子进程并不属于其他命令。尽管这些命令不属于同一个进程组,Ctrl- C扔可以将其全部终止。

一个会话组可以依附于一个终端也可以不依附任何终端,比如守护进程。
发送给会话领导的信号被转发到该会话的所有进程组内,然后再被转发到这些进程组中的所有进程。

建议:如果想要创建一个守护进程,那就应该问自己一个基本的问题:这个进程需要一直保持响应吗?
如果不是,那么就应该考虑定时任务或后台


15、 生成终端进程
fork + exec
exec(2)可以让你将当前进程转变成另外一个进程。这种转变是有去无回的。它不会关闭任何打开的文件描述符(默认情况下)或是进程内存清理。

Process.spawn有很多选项可以用来控制子进程的行为。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值