3. 调整进程的优先级

在保证每个进程都能够顺利执行外,为了让某些任务优先完成,那么系统在进行进程调度时就会采用一定的调度办法,比如常见的有按照优先级的时间片轮转的调度算法。这种情况下,可以通过 renice 调整正在运行的程序的优先级,例如:`

范例:获取进程优先级

$ ps -e -o "%p %c %n" | grep xfs
 5089 xfs               0

范例:调整进程的优先级

$ renice 1 -p 5089
renice: 5089: setpriority: Operation not permitted
$ sudo renice 1 -p 5089   #需要权限才行
[sudo] password for falcon:
5089: old priority 0, new priority 1
$ ps -e -o "%p %c %n" | grep xfs  #再看看,优先级已经被调整过来了
 5089 xfs               1

结束进程

既然可以通过命令行执行程序,创建进程,那么也有办法结束它。可以通过 kill 命令给用户自己启动的进程发送某个信号让进程终止,当然“万能”的 root 几乎可以 kill 所有进程(除了 init 之外)。例如,

范例:结束进程

$ sleep 50 &   #启动一个进程
[1] 11347
$ kill 11347

kill 命令默认会发送终止信号( SIGTERM )给程序,让程序退出,但是 kill 还可以发送其他信号,这些信号的定义可以通过 man 7 signal 查看到,也可以通过 kill -l 列出来。

$ man 7 signal
$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL
 5) SIGTRAP      6) SIGABRT      7) SIGBUS       8) SIGFPE
 9) SIGKILL     10) SIGUSR1     11) SIGSEGV     12) SIGUSR2
13) SIGPIPE     14) SIGALRM     15) SIGTERM     16) SIGSTKFLT
17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU
25) SIGXFSZ     26) SIGVTALRM   27) SIGPROF     28) SIGWINCH
29) SIGIO       30) SIGPWR      31) SIGSYS      34) SIGRTMIN
35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3  38) SIGRTMIN+4
39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12
47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14
51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10
55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7  58) SIGRTMAX-6
59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

范例:暂停某个进程

例如,用 kill 命令发送 SIGSTOP 信号给某个程序,让它暂停,然后发送 SIGCONT 信号让它继续运行。

$ sleep 50 &
[1] 11441
$ jobs
[1]+  Running                 sleep 50 &
$ kill -s SIGSTOP 11441   #这个等同于我们对一个前台进程执行CTRL+Z操作
$ jobs
[1]+  Stopped                 sleep 50
$ kill -s SIGCONT 11441   #这个等同于之前我们使用bg %1操作让一个后台进程运行起来
$ jobs
[1]+  Running                 sleep 50 &
$ kill %1                  #在当前会话(session)下,也可以通过作业号控制进程
$ jobs
[1]+  Terminated              sleep 50

可见 kill 命令提供了非常好的功能,不过它只能根据进程的 ID 或者作业来控制进程,而 pkillkillall 提供了更多选择,它们扩展了通过程序名甚至是进程的用户名来控制进程的方法。更多用法请参考它们的手册。

范例:查看进程退出状态

当程序退出后,如何判断这个程序是正常退出还是异常退出呢?还记得 Linux 下,那个经典 hello world 程序吗?在代码的最后总是有条 return 0 语句。这个 return 0 实际上是让程序员来检查进程是否正常退出的。如果进程返回了一个其他的数值,那么可以肯定地说这个进程异常退出了,因为它都没有执行到 return 0 这条语句就退出了。

那怎么检查进程退出的状态,即那个返回的数值呢?

Shell 中,可以检查这个特殊的变量 $?,它存放了上一条命令执行后的退出状态。

$ test1
bash: test1: command not found
$ echo $?
127
$ cat ./test.c | grep hello
$ echo $?
1
$ cat ./test.c | grep hi
    printf("hi, myself!\n");
$ echo $?
0

貌似返回 0 成为了一个潜规则,虽然没有标准明确规定,不过当程序正常返回时,总是可以从 $? 中检测到 0,但是异常时,总是检测到一个非 0 值。这就告诉我们在程序的最后最好是跟上一个 exit 0 以便任何人都可以通过检测 $? 确定程序是否正常结束。如果有一天,有人偶尔用到你的程序,试图检查它的退出状态,而你却在程序的末尾莫名地返回了一个 -1 或者 1,那么他将会很苦恼,会怀疑他自己编写的程序到底哪个地方出了问题,检查半天却不知所措,因为他太信任你了,竟然从头至尾都没有怀疑你的编程习惯可能会与众不同!

进程通信

为便于设计和实现,通常一个大型的任务都被划分成较小的模块。不同模块之间启动后成为进程,它们之间如何通信以便交互数据,协同工作呢?在《UNIX 环境高级编程》一书中提到很多方法,诸如管道(无名管道和有名管道)、信号(signal)、报文(Message)队列(消息队列)、共享内存(mmap/munmap)、信号量(semaphore,主要是同步用,进程之间,进程的不同线程之间)、套接口(Socket,支持不同机器之间的进程通信)等,而在 Shell 中,通常直接用到的就有管道和信号等。下面主要介绍管道和信号机制在 Shell 编程时的一些用法。

范例:无名管道(pipe)

在 Linux 下,可以通过 | 连接两个程序,这样就可以用它来连接后一个程序的输入和前一个程序的输出,因此被形象地叫做个管道。在 C 语言中,创建无名管道非常简单方便,用 pipe 函数,传入一个具有两个元素的 int 型的数组就可以。这个数组实际上保存的是两个文件描述符,父进程往第一个文件描述符里头写入东西后,子进程可以从第一个文件描述符中读出来。

如果用多了命令行,这个管子 | 应该会经常用。比如上面有个演示把 ps 命令的输出作为 grep 命令的输入:

$ ps -ef | grep init

也许会觉得这个“管子”好有魔法,竟然真地能够链接两个程序的输入和输出,它们到底是怎么实现的呢?实际上当输入这样一组命令时,当前 Shell 会进行适当的解析,把前面一个进程的输出关联到管道的输出文件描述符,把后面一个进程的输入关联到管道的输入文件描述符,这个关联过程通过输入输出重定向函数 dup (或者 fcntl )来实现。

范例:有名管道(named pipe)

有名管道实际上是一个文件(无名管道也像一个文件,虽然关系到两个文件描述符,不过只能一边读另外一边写),不过这个文件比较特别,操作时要满足先进先出,而且,如果试图读一个没有内容的有名管道,那么就会被阻塞,同样地,如果试图往一个有名管道里写东西,而当前没有程序试图读它,也会被阻塞。下面看看效果。

$ mkfifo fifo_test    #通过mkfifo命令创建一个有名管道
$ echo "fewfefe" > fifo_test
#试图往fifo_test文件中写入内容,但是被阻塞,要另开一个终端继续下面的操作
$ cat fifo_test        #另开一个终端,记得,另开一个。试图读出fifo_test的内容
fewfefe

这里的 echocat 是两个不同的程序,在这种情况下,通过 echocat 启动的两个进程之间并没有父子关系。不过它们依然可以通过有名管道通信。

这样一种通信方式非常适合某些特定情况:例如有这样一个架构,这个架构由两个应用程序构成,其中一个通过循环不断读取 fifo_test 中的内容,以便判断,它下一步要做什么。如果这个管道没有内容,那么它就会被阻塞在那里,而不会因死循环而耗费资源,另外一个则作为一个控制程序不断地往 fifo_test 中写入一些控制信息,以便告诉之前的那个程序该做什么。下面写一个非常简单的例子。可以设计一些控制码,然后控制程序不断地往 fifo_test 里头写入,然后应用程序根据这些控制码完成不同的动作。当然,也可以往 fifo_test 传入除控制码外的其他数据。

        0) echo "The CONTROL number is ZERO, do something ..."
            ;;
        1) echo "The CONTROL number is ONE, do something ..."
            ;;
        *) echo "The CONTROL number not recognized, do something else..."
            ;;
    esac
done
  • 控制程序的代码
      $ cat control.sh
      #!/bin/bash
      FIFO=fifo_test
      CI=$1
      [ -z "$CI" ] && echo "the control info should not be empty" && exit
      echo $CI > $FIFO
    
  • 一个程序通过管道控制另外一个程序的工作
      $ chmod +x app.sh control.sh    #修改这两个程序的可执行权限,以便用户可以执行它们
      $ ./app.sh  #在一个终端启动这个应用程序,在通过./control.sh发送控制码以后查看输出
      The CONTROL number is ONE, do something ...    #发送1以后
      The CONTROL number is ZERO, do something ...    #发送0以后
      The CONTROL number not recognized, do something else...  #发送一个未知的控制码以后
      $ ./control.sh 1            #在另外一个终端,发送控制信息,控制应用程序的工作
      $ ./control.sh 0
      $ ./control.sh 4343
    

这样一种应用架构非常适合本地的多程序任务设计,如果结合 web cgi,那么也将适合远程控制的要求。引入 web cgi 的唯一改变是,要把控制程序 ./control.sh 放到 webcgi 目录下,并对它作一些修改,以使它符合 CGI 的规范,这些规范包括文档输出格式的表示(在文件开头需要输出 content-tpye: text/html 以及一个空白行)和输入参数的获取 (web 输入参数都存放在 QUERY_STRING 环境变量里头)。因此一个非常简单的 CGI 控制程序可以写成这样:

#!/bin/bash
FIFO=./fifo_test
CI=$QUERY_STRING
[ -z "$CI" ] && echo "the control info should not be empty" && exit
echo -e "content-type: text/html\n\n"
echo $CI > $FIFO

在实际使用时,请确保 control.sh 能够访问到 fifo_test 管道,并且有写权限,以便通过浏览器控制 app.shhttp://ipaddress\_or\_dns/cgi-bin/control.sh?0

问号 ? 后面的内容即 QUERY_STRING,类似之前的 $1

这样一种应用对于远程控制,特别是嵌入式系统的远程控制很有实际意义。在去年的暑期课程上,我们就通过这样一种方式来实现马达的远程控制。首先,实现了一个简单的应用程序以便控制马达的转动,包括转速,方向等的控制。为了实现远程控制,我们设计了一些控制码,以便控制马达转动相关的不同属性。

在 C 语言中,如果要使用有名管道,和 Shell 类似,只不过在读写数据时用 readwrite 调用,在创建 fifo 时用 mkfifo 函数调用。

范例:信号(Signal)

信号是软件中断,Linux 用户可以通过 kill 命令给某个进程发送一个特定的信号,也可以通过键盘发送一些信号,比如 CTRL+C 可能触发 SGIINT 信号,而 CTRL+\ 可能触发 SGIQUIT 信号等,除此之外,内核在某些情况下也会给进程发送信号,比如在访问内存越界时产生 SGISEGV 信号,当然,进程本身也可以通过 killraise 等函数给自己发送信号。对于 Linux 下支持的信号类型,大家可以通过 man 7 signal 或者 kill -l 查看到相关列表和说明。

对于有些信号,进程会有默认的响应动作,而有些信号,进程可能直接会忽略,当然,用户还可以对某些信号设定专门的处理函数。在 Shell 中,可以通过 trap 命令(Shell 内置命令)来设定响应某个信号的动作(某个命令或者定义的某个函数),而在 C 语言中可以通过 signal 调用注册某个信号的处理函数。这里仅仅演示 trap 命令的用法。

$ function signal_handler { echo "hello, world."; } #定义signal_handler函数
$ trap signal_handler SIGINT  #执行该命令设定:收到SIGINT信号时打印hello, world
$ hello, world     #按下CTRL+C,可以看到屏幕上输出了hello, world字符串

类似地,如果设定信号 0 的响应动作,那么就可以用 trap 来模拟 C 语言程序中的 atexit 程序终止函数的登记,即通过 trap signal_handler SIGQUIT 设定的 signal_handler 函数将在程序退出时执行。信号 0 是一个特别的信号,在 POSIX.1 中把信号编号 0 定义为空信号,这常被用来确定一个特定进程是否仍旧存在。当一个程序退出时会触发该信号。

$ cat sigexit.sh
#!/bin/bash
function signal_handler {
    echo "hello, world"
}
trap signal_handler 0
$ chmod +x sigexit.sh
$ ./sigexit.sh    #实际Shell编程会用该方式在程序退出时来做一些清理临时文件的收尾工作
hello, world