posts - 200, comments - 8, trackbacks - 0, articles - 0

UNIX进程组,会话和作业控制(转)

Posted on 2012-12-09 14:30 鑫龙 阅读(241) 评论(0)  编辑 收藏 引用 所属分类: linux编程
       在UNIX系统中,作业控制:允许在一个终端上启动多个作业(进程组),控制哪一个作业可以存取该终端,以及哪些作业在后台运行。一句话就是,作业控制是解决不同作业(也就是进程组)对控制终端这个资源的使用的竞争问题。作业控制作为Shell的一个特性存在,也就是说有的shell支持作业控制这个作业功能,有的不支持。linux下常用的bash是支持的作业控制功能的(通常我们使用& bg fg 等)。另外为了支持作业控制,引入了进程组,会话期,控制终端等概念,还需要内核以一定的信号支持。

一. 进程组、会话与终端

(1).每个进程都属于一个进程组。进程组是一个或多个进程的集合,通常它们与一组作业相关联,可以接受来自同一终端的各种信号。每个进程组都有唯一的进程组ID(整数,也可以存放在pid_t类型中)。

    #include <unistd.h>

    pid_t getpgrp(void);

       //返回值;调用进程的进程组ID

    每个进程组都有一个组长进程,组长进程的标识是进程组ID等于其进程ID。组长进程可以创建一个进程组、创建该组中的进程。只有某个进程中有一个进程存在,则该进程就存在,与组长进程是否终止无关。从进程组创建开始到其中最后一个进程离开为止的时间区间成为进程组的生存期。进程组中最后一个进程可以终止或者转移到另一个进程组中。

    进程调用setpgid(setsid也可以)可以参加一个现存的组或者创建一个新进程组

    #include <sys/types.h>

    #include <unistd.h>

    int setpgid(pid_t pid, pid_t pgid);

        //返回:若成功则为0,出错为-1

    这将pid进程的进程组ID设置为pgid。如果pid是0,则使用调用者的进程ID。另外,如果pgid是0,则由pid指定的进程ID被用作为进程组ID。如果这两个参数相等,则由pid指定的进程变成进程组组长。

    一个进程只能为它自己或它的子进程设置进程组I D。在它的子进程调用了exec后,它就不再能改变该子进程的进程组I D。

    在大多数作业控制shell中,在fork之后调用此函数,使父进程设置其子进程的进程组ID,然后使子进程设置其自己的进程组ID。这些调用中有一个是冗余的,但这样做可以保证父、子进程在进一步操作之前,子进程都进入了该进程组。否则依赖于哪一个进程先执行,就产生一个竞态条件。


    (2).session是一个或多个进程组的集合。

    例如,在shell中:

    $proc1 | proc2 &

    $proc3 | proc4 

那么此时,session中就会有三个进程组存在,分别是{登陆shell(session leader)},{proc1, proc2}, {proc3, proc4}。   

    进程调用setsid函数就可建立一个新对话期。

    #include <sys/types.h>

    #include <unistd.h>

    pid_t setsid(void);

    如果调用此函数的进程不是一个进程组的组长,则此函数创建一个新对话期,结果为:

(a) 此进程变成该新对话期的对话期首进程(session leader,对话期首进程是创建该对话期的进程)。此进程是该新对话期中的唯一进程。

(b) 此进程成为一个新进程组的组长进程。新进程组ID是此调用进程的进程ID。

(c) 此进程没有控制终端。如果在调用setsid之前此进程有一个控制终端,那么这种联系也被解除。

    如果此调用进程已经是一个进程组的组长,则此函数返回出错。为了保证不处于这种情况,通常先调用fork,然后使其父进程终止,而子进程则继续。因为子进程继承了父进程的进程组ID,而其进程ID则是新分配的,两者不可能相等,所以这就保证了子进程不是一个进程组的组长。

    对话期和进程组有一些其他特性:

    • 一个对话期可以有一个单独的控制终端(controlling terminal)。这通常是我们在其上登录的终端设备(终端登录情况)或伪终端设备(网络登录情况)。

    • 建立与控制终端连接的对话期首进程,被称之为控制进程(controlling process)。

    • 一个对话期中的几个进程组可被分成一个前台进程组(foreground process group)以及一个或几个后台进程组(background process group)。前台进程组接受终端输入信号。Shell中的作业控制就是对前后台进程组的控制,&或Ctrl+Z的进程组就是后台进程组。

    • 如果一个对话期有一个控制终端,则它有一个前台进程组,其他进程组则为后台进程组。

    • 无论何时键入中断键(常常是DELETE或Ctrl-C)或退出键(常常是Ctrl-\),就会造成将中断信号或退出信号送至前台进程组的所有进程。

    • 终端的挂断信号送至控制进程(对话期首进程。)

    • 系统在登陆时将自动建立控制终端。

    如何分配一个控制终端依赖于实现。在open时,有几个和控制终端相关的选项:O_NOCTTY 如果要打开的文件为终端机设备时,则不会将该终端当成进程控制终端。

    有时不管标准输入、标准输出是否重新定向,程序都要与控制终端交互作用。保证程序读写控制终端的方法是打开文件/dev/tty,在内核中,此特殊文件代表控制终端。如果程序没有控制终端,则打开此设备将失败。

    注意:控制终端只有一个,通常控制终端/dev/tty代表当前shell的控制终端,其实是一个指向实际终端设备的连接。实际的终端设备可能是tty1,ttyS1或者pst/1.

(3).控制终端与终端

    首先介绍两个抽象概念:

    tty(终端设备的统称):tty一词源于Teletypes,或者teletypewriters,原来指的是电传打字机,是通过串行线用打印机键盘通过阅读和发送信息的东西,后来这东西被键盘与显示器取代,所以现在叫终端比较合适。终端是一种字符型设备,它有多种类型,通常使用tty来简称各种类型的终端设备。

    pty(伪终端,虚拟终端):远程telnet到主机或使用xterm时不也需要一个终端交互。

    在Linux系统的设备特殊文件目录/dev/下,终端特殊设备文件一般有以下几种:

  1、串行端口终端(/dev/ttySn) 串行端口终端(Serial Port Terminal)是使用计算机串行端口连接的终端设备。计算机把每个串行端口都看作是一个字符设备。有段时间这些串行端口设备通常被称为终端设备,因为那时它的最大用途就是用来连接终端。

   2、伪终端(/dev/pty/) 伪终端(Pseudo Terminal)是成对的逻辑终端设备(即master和slave设备, 对master的操作会反映到slave上)。例如/dev/ptyp3和/dev/ttyp3(或者在设备文件系统中分别是/dev/pty /m3和/dev/pty/s3)。它们与实际物理设备并不直接相关。如果一个程序把ptyp3(master设备)看作是一个串行端口设备,则它对该端口的读/ 写操作会反映在该逻辑终端设备对应的另一个ttyp3(slave设备)上面。而ttyp3则是另一个程序用于读写操作的逻辑设备。telnet主机A就是通过“伪终端”与主机A的登录程序进行通信。

    3、控制终端(/dev/tty) 如果当前进程有控制终端(Controlling Terminal)的话,那么/dev/tty就是当前进程的控制终端的设备特殊文件。可以使用命令”ps –ax”来查看进程与哪个控制终端相连。对于你登录的shell,/dev/tty是你当前的控制终端,设备号是(5,0)。使用命令”tty”可以查看它具体对应哪个实际终端设备。/dev/tty有些类似于到实际所使用终端设备的一个联接。在当前的控制终端的读写都会写到当前的终端设备中,例如echo "hello" > /dev/tty ,都会直接显示在当前的终端中。而cat </dev/tty会从当前终端读取输入(行缓冲)并输出出来。

    4、控制台终端(/dev/ttyn, /dev/console) 在Linux 系统中,计算机显示器通常被称为控制台终端(Console)。它仿真了类型为Linux的一种终端(TERM=Linux),并且有一些设备特殊文件与之相关联:tty0、tty1、tty2 等。当你在控制台上登录时,使用的是tty1。使用Alt+[F1—F6]组合键时,我们就可以切换到tty2、tty3等上面去。tty1–tty6等称为虚拟终端,而tty0则是当前所使用虚拟终端的一个别名,系统所产生的信息会发送到该终端上(这时也叫控制台终端)。因此不管当前正在使用哪个虚拟终端,系统信息都会发送到控制台终端上。/dev/console即控制台,是与操作系统交互的设备,系统将一些信息直接输出到控制台上。目前只有在单用户模式下,才允许用户登录控制台。

    5、虚拟终端(/dev/pts/n)在Xwindows模式下的伪终端.如我在Kubuntu下用konsole,就是用的虚拟终端,用tty命令可看到/dev/pts/1。

    6、其它类型Linux系统中还针对很多不同的字符设备存在有很多其它种类的终端设备特殊文件。例如针对ISDN设备的/dev/ttyIn终端设备等。

    

(4).几个常用的终端相关命令

(a). 在ubuntu等发行版本中,图形界面下Ctrl+Alt+F1-F6是打开tty1-6的终端()。在tty1-6这些终端下Alt1-6是切换终端,Alt+F7进入图形界面。

 

(b). 可以通过ps -t的方式查看其他终端进程(这些终端在初始进入时候属于getty状态,由于tty1终端尚未登录所以运行getty。而且没有其他进程使用tty1终端):

    $ps -t tty1

    PID TTY          TIME CMD

    1524 tty1     00:00:00 getty

 

(c). 可以使用shell的tty命令来识别现在使用的终端:

    $ tty

    /dev/pts/0

 

(d). stty - set tty, change and print terminal line settings

$stty -a    命令用于检查和修改当前控制终端的通信参数。UNIX系统为键盘的输入和终端的输出提供了重要的控制手段,可以通过stty命令对特定终端或通信线路设置选项.

$stty tostop        #[-]tostop  STOP尝试向终端写入数据的后台任务。(SIGTTOU)

$echo "hello world" &   #试图输出的进程会被终止

[1] 3063

$ fg

echo "hello world"

hello world

 

$ stty -tostop

$ echo "hello world" &

[1] 3065

hello world     #不STOP结果直接被输出出来。

$ jobs

[1]+  完成                 echo "hello world"

所有选项,-option_name是关闭,option_name是打开。对于控制终端的设置也是管理中重要的工作之一。

 

(4).需要有一种方法来通知内核哪一个进程组是前台进程组,这样,终端设备驱动程序就能了解将终端输入和终端产生的信号送到何处。

    #include <termios.h>

    #include <unistd.h>

    int tcgetattr(int fd, struct termios *termios_p);   //成功则返回与终端文件描述符fd相关联的前台进程的组ID,出错则返回-1。

    int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);   //成功则返回0,出错则返回-1

    //struct termios定义一和终端相关的标识字段,例忽略BREAK键,忽略校验等等。

    大多数应用程序不直接调用这两个函数,它们通常由作业控制shell调用。

---
---
---
---------------------------
---
---
---
---------------------------
---
---
---
---------------------------
---
---
---
---------------------------
二. 作业控制

(1).允许在一个终端上起动多个作业(进程组),控制哪一个作业可以存取该终端,以及哪些作业在后台运行。作业控制要求三种形式的支持:

(a).支持作业控制的shell。

(b).内核中的终端驱动程序必须支持作业控制。

(c).必须提供对某些作业控制信号的支持。

三个特殊字符可使终端驱动程序产生信号,并将它们送至前台进程组,它们是:

• 中断字符(一般采用DELETE或Ctrl-C)产生SIGINT。

• 退出字符(一般采用Ctrl-\)产生SIGQUIT。

• 挂起字符(一般采用Ctrl-Z)产生SIGTSTP。

(2).不支持作业控制的Shell

    对于不支持作业控制的Shell,例如bsh,它的命令和它自身的进程处于同一个会话和前台进程组。在后台执行的命令(&)和管道命令的进程依然和Shell是同一个进程组。

    如果一个后台进程试图取走终端,例如cat > temp &。在有作业控制时,后台作业被放在后台进程组中。如果后台作业试土读控制终端,则会产生信号SIGTTIN。在没有作业控制时,其处理方法是如果该进程自己没有重定向标准输入,则Shell会自动将标准输入重定向到/dev/null。读/dev/null则会产生一个EOF让cat读到文件末尾,正常结束。另外,管道执行的结构图如下:

<图>

(3).支持作业控制的Shell

$ ps -o pid -o ppid -o sid -o pgid -o command

  PID  PPID   SID  PGID COMMAND

 2074  2068  2074  2074 /bin/bash

 2580  2074  2074  2580 ps -o pid -o ppid -o sid -o pgid -o command

可以看出它们有不同的PGID。对于管道命令,他们属于同一个进程组:

$ ps -o pid -o ppid -o sid -o pgid -o command | cat

  PID  PPID   SID  PGID COMMAND

 2074  2068  2074  2074 /bin/bash

 2584  2074  2074  2584 ps -o pid -o ppid -o sid -o pgid -o command

 2585  2074  2074  2584 cat

(4).SIGHUP信号

    SIGHUP会在以下3种情况下被发送给相应的进程:

1、终端关闭时,该信号被发送到session首进程以及作为job提交的进程(即用& 符号提交的进程)

2、session首进程退出时,该信号被发送到该session中的前台进程组中的每一个进程

3、若父进程退出导致进程组成为孤儿进程组,且该进程组中有进程处于停止状态(收到SIGTSTP信号),SIGHUP会被发送到该进程组中的每一个进程。

    系统对SIGHUP信号的默认处理是终止收到该信号的进程。所以若程序中没有捕捉该信号,当收到该信号时,进程就会退出。

    If a controlling process exits, the system revokes further access to the controlling terminal and sends a SIGHUP signal to the foreground process group. If a process such as a job-control shell exits, each process group that it created will become an orphaned process group。

(5).bash作业控制命令

(a). nohup:使用nohup让程序永远后台运行

   由于很多程序不是守护进程,我们又想让它在后台运行,不受SIGHUP信号影响(例如shell退出或者终端连接断开),那么使用nohup命令。

    $nohup sleep 100 &

    $appending output to nohup.out  #无论是否将命令重定向输出输出终端,输出都将附加到当前目录的nohup文件中。

    $ps -t pts/0

    #可以看到sleep 100进程终端为pts/0

    $exit

   然后打开另一终端:

   $ tty

    /dev/pts/1

    $ ps -ef | grep sleep | grep -v grep

    luffy     4171     1  0 18:12 ?        00:00:00 sleep 100

    #100秒后再次查询,结果为空。说明进程正常退出   

(b). 作业号%n:支持作业控制的Shell可以识别%+作业号的作业进程

例如:

$ sleep 10&

[1] 4294

$ %1        #bring %1 to front,same as fg %1

sleep 10

(c).$fg 则将第一个作业放到前台。fg %n

(d).bg 将一个在后台暂停的命令,变成继续执行 如果后台中有多个命令,可以用bg %jobnumber将选中的命令调出,%jobnumber是通过jobs命令查到的后台正在执行的命令的序号(不是pid)。

(e).jobs

(f). ctrl+z or &

 

(6).Linux让进程在后台执行

(a). nohup命令:略

(b). setsid命令. 如果我们的进程不属于接受HUP 信号的终端的子进程,那么自然也就不会受到HUP 信号的影响了。setsid 就能帮助我们做到这一点。系统调用setsid()请见前面。

$setsid ping www.baidu.com

$ PING www.a.shifen.com (119.75.218.70) 56(84) bytes of data.

64 bytes from 119.75.218.70: icmp_req=1 ttl=54 time=1.88 ms

......

^C

64 bytes from 119.75.218.70: icmp_req=3 ttl=54 time=1.65 ms

64 bytes from 119.75.218.70: icmp_req=4 ttl=54 time=1.92 ms

......

输出信息会不断出现在这个终端,由于这个进程已经不是这个Shell的会话了,所以Ctrl+C不能终止这个进程。就算关闭整个终端,程序也会继续执行。

在另一个终端将它终止 :

$ ps -e -o pid -o sid -o pgid -o command | grep ping | grep -v grep

 4474  4474  4474 ping www.baidu.com

#可以看出sid=pid.由于是

$ kill -SIGKILL 4396

$ ps -ef | grep ping |grep -v grep

进程被kill掉。

(c).(&)

subshell:一个或多个命令包含在()里执行就能让他们在子shell中执行。所以让在子shell中执行的作业用jobs看不到,自然也不会接收任何hup信号。例如:

$(ping www.baidu.com &)

(d).disown

对于已经提交出去的命令,使用作业调度disown来达到目的:

用disown -h jobspec 来使某个作业忽略HUP信号。

用disown -ah 来使所有的作业都忽略HUP信号。

用disown -rh 来使正在运行的作业忽略HUP信号。

# cp -r testLargeFile largeFile &

[1] 4825

# jobs

[1]+  Running                 cp -i -r testLargeFile largeFile &

# disown -h %1

# ps -ef |grep largeFile

root      4825   968  1 09:46 pts/4    00:00:00 cp -i -r testLargeFile largeFile

root      4853   968  0 09:46 pts/4    00:00:00 grep largeFile

# logout  

对于正在运行,没有加&放到后台运行的程序可以先ctrl+Z停止进程,然后用bg %n让这个作业继续执行。再用disown忽略hup信号。

(e). screen终端模拟器,功能强大这里只简单介绍。

没有启动screen的进程树 :

$ ping www.baidu.com >~/temp.output &

[1] 4962

$ pstree -H 4962        #让指定的进程高亮显示

init─┬─/usr/bin/termin─┬─bash

     │                ├─bash─┬─ping

......

在使用了screen的进程树

# screen -r Urumchi

# ping www.ibm.com &

[1] 9488

# pstree -H 9488

init-+-/usr/bin/termin-+-bash

     |                 |-bash-+-ping

     |                 |      `-screen---screen-+-bash

 

Reference:

APUE

man

Stty使用一技    http://fanqiang.chinaunix.net/a1/b4/20020606/060200245.html

Linux中的终端、控制台、tty、pty等概念    http://news.newhua.com/news1program_language/2010/623/10623141048745773199BCF0CFH6AKB9930IGCFKHBH4IBE65IDFI07F.html

IBM文库-Linux 技巧:让进程在后台可靠运行的几种方法http://www.ibm.com/developerworks/





只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理