Unix 进程
博客专区 > QMonkey 的博客 > 博客详情
Unix 进程
QMonkey 发表于3年前
Unix 进程
  • 发表于 3年前
  • 阅读 19
  • 收藏 1
  • 点赞 0
  • 评论 0

腾讯云 技术升级10大核心产品年终让利>>>   

组成:用户空间和内核
程序不可直接访问内核,所有的通信都是通过系统调用完成的。系统调用为内核和用户空间搭建了桥梁。它规定了程序与计算机硬件之间所允许发生的一切交互。




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




进程皆有父:
系统中运行的每一个进程都有对应的父进程。每个进程都知道其父进程的标识符(称为ppid)。


系统调用:
getppid(2)




进程皆有文件描述符:
万物皆为文件,描述符代表资源:
打开一个资源,就会获得一个文件描述符编号。文件描述符并不会在无关进程之间共享,它只存在于其所属的进程之中。当进程结束后,会和其他由进程所打开的资源一同被关闭。
进程打开的所有资源都会获得一个用于标识的唯一数字。这便是内核跟踪进程所用资源的方法。


标准流文件描述符:
STDIN      1
STDOUT   2
STDERR    3


系统调用:
open(2),close(2),read(2),write(2),pipe(2),fsync(2),stat(2)




进程皆有资源限制:
进程可以拥有多少个文件描述符取决于你的系统配置,不过重要的一点是:内核为进程施加了某些资源限制。


软限制和硬限制:
如果超出了软限制,就会产生一次异常。对于硬限制,通常情况下,只有超级用户能修改硬限制。sysctl(8)可更改系统级别的限制。


实践领域:
多数程序通常并不需要修改系统资源限制。但对于一些特殊的工具这可是至关重要的。其中一种情况就是某个进程需要处理成千上万的并发网络连接。


系统调用:
getrlimit(2),setrlimit(2)




进程皆有环境:
环境变量就是包含数据的键-值对。
所有进程都从其父进程处继承环境变量。环境变量对于特定进程而言是全局性的。


实践领域:
环境变量经常作为一种将输入传递到命令行程序中的通用方法。


C库函数:
setenv(3),getenv(3)
参考:
environ(7)




进程皆有参数:
所有进程都可以访问名为ARGV的特殊数组。其他编程语言可能在实现方式上略微不同,但是都会有argv。
argv是argument vector的缩写,换句话说就是一个参数向量或数组。它保存了在命令行中传递给当前进程的参数。


实践领域:
ARGV最常见的用例大概就是将文件名传入程序。写一个在命令行中接受一个或多个文件名作为参数并进行处理的程序。




进程皆有名:
有两种运作在进程自身层面上的机制可以用来互通信息。一个是进程名称,另一个是退出码。




进程皆有退出码:
当进程即将结束时,它还有最后一线机会留下自身的信息:退出码。所有进程在退出的时候都带有数字退出码(0-255),用于指明进程是否顺利结束。
按惯例,退出码为0的进程被认为是顺利结束;其他的退出码则表明出现了错误,不同的退出码代表不同的错误。
尽管退出码通常用来表明不同的错误,它们其实是一种通信途径。你只需要以适合自己程序的方式来处理各种进程退出码,便打破了传统。




进程皆可衍生:
fork(2)系统调用允许运行中的进程以编程的形式创建新的进程。这个新进程与原始进程一模一样。
进行衍生时,调用fork(2)的进程被成为“父进程”,新创建的进程成为“子进程”。
子进程从父进程处继承了其所占用内存中的所有内容,以及所有属于父进程的已打开的文件描述符。这样两个进程就可以共享打开的文件、套接字,等等。
子进程可以随意更改其内存内容的副本,而不会对父进程造成任何影响。因为“写时复制”技术。


多核编程:
通过生成新的进程,你的代码可以(不能完全保证)被分配到多个CPU核心中。
在配置了4个CPU的系统中,如果衍生出4个新进程,那么这些进程分别由不同的CPU来处理,从而实现多核并发。然而,并不保证它们会并行操作。在繁忙的系统中,有可能4个进程都由同一个CPU来处理。


注意:
fork(2)创建了一个和旧进程一模一样的新进程。所以如果一个使用了500MB内存的进程进行了衍生,那么就有1GB的内存被占用了。
重复同样的操作十次,你很快就会耗尽所有的内存。这通常被成为“fork炸弹”。使用并发执行前,请务必确保你知道这样做的后果。


系统调用:
fork(2)




孤儿进程:
子进程在父进程结束前结束,于是子进程就变成孤儿进程。此时操作系统并不会结束子进程。




有好的进程:
Cow(copy-on-write):
fork(2)创建了一个和父进程一模一样的子进程。它包含了父进程在内存中的一切内容。实实在在地复制所有的数据所产生的系统开销不容小觑,因此现代的Unix系统采用写时复制的方法来克服这个问题。
Cow将实际的内存复制操作推迟到了真正需要写入的时候。




进程可待:
看顾:
对于其他多数涉及fork(2)的用例来说,你会希望有一些能够监视子进程动向的方法。在Ruby中,Process.wait就提供了这么一种技术。
Process.wait是一个阻塞调用,该调用使得父进程一直等待它的某个子进程退出之后继续执行。


竞争条件:
当一个子进程退出时,处理某个退出进程的代码还在运行,在这时另一个进程也退出了,会怎么样?这项技术能够避免竞争条件。内核将退出的进程信息加入队列,这样一来父进程就总是能够依照子进程推出的顺序接受到信息。


实践领域:
关注子进程的理念是一般的Unix编程模式的核心。这种模式有时候被称为看顾进程,master/worker或者perforking。


此模式的核心是这样一种概念:你有一个衍生出多个并发子进程的进程,这个进程看管着这些子进程,确保它们能够保持响应,并对子进程的推出作出回应等等。


系统调用:
wait(2),waitpid(2)




僵尸进程:
内核会将已退出的子进程的状态信息加入队列。所以即便你在子进程推出很久之后才调用Process.wait,依然可以获取它的状态信息。即内核会一直保留已推出的子进程的状态信息,直到父进程使用Process.wait请求这些信息。如果父进程一直不发出请求,那么状态信息就会被内核一直保留着。因此创建即发即弃的子进程,却不去读取状态信息,便是在浪费内核资源。
如果不想用Process.wait来等待某个子进程退出,那么你就需要分离这个子进程。
Process.detach(pid) 做了些什么?它不过是生成了一个新线程,这个线程的唯一工作就是等待由pid所指的那个子进程退出。这确保了内核不会一直保留那些我们不需要的状态信息。


实践领域:
任何已经结束的进程,如果它的状态信息一直未能被读取,那么它就是一个僵尸进程。所以任何子进程如果在结束之时其父进程仍在运行,那么这个子进程很快就会成为僵尸。一旦父进程读取了僵尸进程的状态信息,那么它就不复存在,也就不再消耗内核资源。


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




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


一个繁忙的父进程会怎么做?可不是每个父进程都有闲暇一直等待自己的子进程结束。对此倒是有一种解决方案,就是Unix信号。父进程在获取到Unix信号之后,就可以调用Process.wait以及时获取子进程状态信息。


SIGCHLD与并发
注意:信号投递是不可靠的。如果你的代码正在处理CHLD信号,这时候另一个子进程结束了,那么你未必能收到第二个CHLD信号。如果同一个信号在极短的间隔内被多次接收到,就会出现这种情况。不过至少总能接收到一次信号。


要正确处理CHLD,你必须在一个循环中调用Process.wait,查找所有已经结束的子进程,这是因为在进入信号处理程序之后,你可能会收到多个CHLD信号。但是,Process.wait不是一个阻塞调用吗?如果只有一个已结束的子进程,而我却调用了两次Process.wait,又该如何避免阻塞整个进程呢?
解决方法:
Process.wait的第二个参数设置为Process::WNOHANG    //非阻塞


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


信号来自何方?
从技术上说,信号由内核发送。准确来说,信号是由一个进程发送到另一个进程,只不过是借用内核作为中介。


信号的最初目的是用来制定终结进程的不同方式。
Process::kill(:INT,<pid of first session>);
因此第二个进程会向第一个进程发送INT信号,使其退出。INT是INTERRUPT(中断)的缩写。
当一个进程接收到这个信号时,系统默认该进程应当中断当前操作并立即退出。


信号一览:
命名信号的时候,名字中的SIG部分是可选的。如:
Term: 表示进程会立即结束
Core: 表示进程会立即结束并进行核心转储(栈跟踪)
Ign: 表示进程会忽略该信号
Stop: 表示进程会停止运行(暂停)
Cont: 表示进程会回复运行(继续)


Unix系统支持的信号(没一个Unix进程都能够响应这些信号,这些信号也都可以被发送到任意的进程):
信号            值        动作                                    注释
SIGHUP       1        Term               由控制终端或控制进程终止时发出
SIGINT        2        Term               来自键盘的中断信号(通常是Ctrl+C)
SIGQUIT      3        Core                来自键盘的退出信号(通常是Ctrl+/)
SIGILL         4        Core                非法指令
SIGABRT     6        Core                来自abort(3)的终止信号
SIGFPE        8        Core                浮点数异常
SIGKILL       9        Term               kill信号
SIGSEGV     11      Core                非法内存地址引用
SIGPIPE       13      Term               管道损坏(Broken pipe):向没有读取进程                              
                                                    的管道写入信息
SIGALRM     14      Term               来自alarm(2)的计时器到时信号
SIGTERM     15      Term               终止信号
SIGUSR1 30,10,16 Term              用户自定义信号1
SIGUSR2 31,12,17 Term              用户自定义信号2
SIGCHLD 20,17,18  IGN               子进程停止或终止
SIGCONT 19,18,25  Cont              如果停止,则继续执行
SIGSTOP 17,19,23  Stop              停止进程执行(来自终端)
SIGTSTP  18,20,24  Stop              来自终端的停止信号
SIGTTIN   21,21,16  Stop              后台进程的终端输入
SIGTTOU 22,22,27  Stop              后台进程的终端输出


SIGKILL和SIGSTOP信号不能被捕获、阻塞或忽略。


重定义信号:
Ruby ex:


第一个ruby会话:
puts Process.pid
trap(:INT) { print "Hello" }
sleep      #以便有时间发送信号


第二个ruby会话:
Process.kill(:INT,<pid of first session>);


试着用Ctrl+C来终结地一个会话,但结果还是一样!


忽略信号:


第一个ruby会话改成:
puts Process.pid
trap(:INT,"IGNORE")
sleep


信号处理程序是全局性的:
捕获一个信号有点像使用一个全局变量,你有可能把其他代码所依赖的东西给修改了。和全局变量不同的是,信号处理程序并没有命名空间。


恰当地重定义信号处理程序:
有一个方法可以保留其他Ruby代码定义的处理程序,这样你的信号处理程序就不会破坏其他已经定义好的处理程序了。这个方法如下:
trap(:INT) { puts "This is the first signal handler" }


old_handler = trap(:INT) {
    old_handler.call
    puts "This is the second handler"
    exit
}
sleep


从最佳实践的角度来说,你的代码不应该定义任何信号处理程序,除非它是服务器。正如一个从命令行启动的长期运行的进程,库代码极少会捕获信号。


实践领域:
有了信号,一旦知道对方的pid,系统中的进程便可以彼此通信。这使得信号成为了一种极其强大的通信工具。常见的用法是在shell中使用kill(1)发送信号。


在实践中,信号多是由长期运行的进程使用,例如服务器和守护进程。多数情况下,发送信号的都是人类用户而非自动化程序。


系统调用:
kill(2),wait(2),sigaction(2),signal(7)




进程皆可互通:
管道:
管道是一个单向的数据流。在读取之前关闭writer,就会将一个EOF(文件结束标志)放入管道中,reader获得原始数据后就会停止读取,而不会一直等待。


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


Ruby ex:
reader,writer = IO.pipe


fork do
    reader.close
    10.times do
        writer.puts "Another one bites the dust"
    end
end


writer.close
while message = reader.gets
    $stdout.puts message
end


请注意,我们关闭了管道未使用的一端,以免干扰正在发送的EOF。如今涉及到两个进程,在考虑EOF时就需要再多考虑一层。因为文件描述符会被复制,所以现在就出现了4个文件描述符。其中只有两个会被用于通信,其他两个必须关闭。因此多余的文件描述符就被关闭了。


流和消息
当使用诸如管道或TCP套接字这样的IO流时,你将数据写入流中,之后跟着一些特定协议的分隔符(delimiter)。比如HTTP使用一连串终止符来分隔头部和主体。
随后从IO流中读取数据时,一次读取一块(chunk),遇到分隔符就停止读取。


也可以用消息来代替流进行通信。我们没办法在管道中使用消息,不过在Unix套接字中就可以。简单来说,Unix套接字是一种只能用于同一台物理主机中进行通信的套接字。它比TCP套接字快得多,非常适合用于IPC。


这些套接字并不使用流,而是使用数据报(datagram)通信。在这种方式中,你向其中一个套接字写入整个消息,然后从另一个套接字中读取整个消息,不需要分隔符。


实践领域:
管道和套接字都是对进程间通信的有益抽象。它们即快速又简单,多被用作通信通道,来代替更为原始的方法,如共享数据库或这日志文件。


系统调用:
socketpair(2),recv(2),send(2)




守护进程:
守护进程是在后台运行的进程,不受终端用户控制。Web服务器或数据库服务器都属于常见的守护进程,它们一直在后台运行响应请求。
守护进程也是操作系统的核心功能。有很多进程一直在后台运行以保证系统正常运行。比如GUI系统的窗口服务器。


首个进程
有一个特殊的守护进程对于操作系统的意义重大。当内核被引导时会产生一个叫做init的进程。这个进程的ppid是0,作为所有进程的祖父。它是首个进程,没有祖先。它的pid是1。


逐步将进程变成守护进程
exit if fork
这一行代码灵活运用了fork方法的返回值。这就意味着父进程将会退出,已成为孤儿的子进程继续照常运行。孤儿进程的ppid始终是1。这是内核能够确保一直运行的唯一进程。
Process.setsid
调用Process.setsid完成了一下三件事:
(1)该进程变成一个新会话的会话领导。
(2)该进程变成一个新进程组的组张。
(3)该进程没有控制终端。


进程组和会话组
进程组和会话组都和作业控制有关。


每一个进程都属于某个组,每个组都有唯一的整数id。进程组是一个相关进程的集合,通常是父进程与其子进程。但是你也可以按照需要将进程分组,只要使用Process.setpgrp(new_group_id)来这只进程的组的id即可。通常情况下,进程组id和进程组组长的pid相同。




生成终端进程:
fork(2)+exec(2)
exec(2)非常简单,它允许你使用另一个进程来替代当前进程。
你可以使用fork(2)创建一个新进程,然后用exec(2)把这个进程变成其他你想要的进程。你的当前进程仍像从前一样运行,也仍可以根据需要生产其他进程。
如果程序依赖于exec(2)调用的输出,Process.wait可以确保你的程序一直等到子进程完成它的工作,这样就能取回结果。


exec(2)不会关闭任何打开的文件描述符(默认情况下)或是进行内存清理。


exec的参数

共有 人打赏支持
粉丝 0
博文 8
码字总数 8066
×
QMonkey
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: