shell编程范例之进程的操作

shell编程范例之进程的操作
by falcon<[email protected]>
2008-02-21

    这一小节写了很久,到现在才写完。本来关注的内容比较多,包括程序开发过程的细节、ELF格式的分析、进程的内存映像等,后来搞得“雪球越滚越大”,甚至 脱离了shell编程关注的内容。所以呢,想了个小办法,“大事化小,小事化了”,把涉及到的内容分成如下几个部分:
    1、把VIM打造成源代码编辑器(源代码编辑过程:用VIM编辑代码的一些技巧)
    2、GCC编译的背后 第一部分:预处理和编译 第二部分:汇编和链接(编译过程:预处理、编译、汇编、链接)
    3、程序执行的那一刹那 (执行过程:当我们从命令行输入一个命令之后)
    4、进程的内存映像 (进程加载过程:程序在内存里是个什么样子)
    5、动态符号链接的细节(动态链接过程:函数puts/printf的地址在哪里)
    6、代码测试、调试与优化小结(程序开发过后:内存溢出了吗?有缓冲区溢出?代码覆盖率如何测试呢?怎么调试汇编代码?有哪些代码优化技巧和方法呢?)
    7、为你的可执行文件“减肥”(从"减肥"的角度一层一层剖开ELF文件)
    8、进程和进程的基本操作(本小节)
    呵呵,好多。终于可以一部分一部分地完成了,不会再有一种对着一个大蛋糕却不知道如何下口的尴尬了。
    进程作为程序真正发挥作用时的“形态”,我们有必要对它的一些相关操作非常熟悉,这一节主要描述进程相关的概念和操作,将介绍包括程序、进程、作业等基本概念以及进程状态查询、进程通信等相关的基本操作等。

1、什么是程序,什么又是进程

    程序是指令的集合,而进程则是程序执行的基本单元。为了让程序完成它的工作,我们必须让程序运行起来成为进程,进而利用处理器资源,内存资源,进行各种I/O操作,从而完成某项指定的工作。
    在这个意思上说,程序是静态的,而进程则是动态的。
    而进程有区别于程序的地方还有,进程除了包含程序文件中的指令数据意外,还需要在内核中有一个数据结构用以存放特定进程的相关属性,以便内核更好的管理和调度进程,从而完成多进程协作的任务。因此,从这个意义上可以说“高于”程序,超出了程序指令本身。
    如果进行过多进程程序的开发,你又会发现,一个程序可能创建多个进程,通过多个进程的交互完成任务。在Linux下,多进程的创建通常是通过fork系统调用实现的。从这个意义上来说程序则”包含”了进程。
    另外一个需要明确的是,程序可以由多种不同的程序语言描述,包括C语言程序、汇编语言程序和最后编译产生的机器指令等。
   
    下面我们简单讨论一下Linux下面如何通过shell进行进程的相关操作。

2、进程的创建

    通常在命令行键入某个程序文件名以后,一个进程就被创建了。例如,

Quote:

$ sleep 100 &            #让sleep程序在后台运行
[1] 9298
$ pidof sleep             #用pidof可以查看指定程序名的进程ID
9298
$ cat /proc/9298/maps    #查看进程的内存映像
08048000-0804b000 r-xp 00000000 08:01 977399     /bin/sleep
0804b000-0804c000 rw-p 00003000 08:01 977399     /bin/sleep
0804c000-0806d000 rw-p 0804c000 00:00 0          [heap]
b7c8b000-b7cca000 r--p 00000000 08:01 443354    
...
bfbd8000-bfbed000 rw-p bfbd8000 00:00 0          [stack]
ffffe000-fffff000 r-xp 00000000 00:00 0          [vdso]


    当一个程序被执行以后,程序被加载到内存中,成为了一个进程。上面显示了该进程的内存映像(虚拟内存),包括程序指令、数据,以及一些用于存放程命令行参数、环境变量的栈空间,用于动态内存申请的堆空间都被分配好了。
    关于程序在命令行执行过程的细节,请参考《Linux命令行下程序执行的那一刹那》。

    实际上,创建一个进程,也就是说让程序运行,还有其他的办法,比如,通过一些配置让系统启动时自动启动我们的程序(具体参考"man init"),或者是通过配置crond(或者at)让它定时启动我们的程序。除此之外,还有一个方式,那就是编写shell脚本,把程序写入一个脚本文 件,当执行脚本文件时,文件中的程序将被执行而成为进程。这些方式的细节就不介绍了,下面介绍如何查看进程的属性。

    需要补充一点的是,在命令行下执行程序时,我们可以通过ulimit内置命令来设置进程可以利用的资源,比如进程可以打开的最大文件描述符个数,最大的栈空间,虚拟内存空间等。具体用法见"help ulimit"。

3、查看进程的属性和状态

    我们可以通过ps命令查看进程的相关属性和状态,这些信息包括进程所属用户,进程对应的程序,进程对cpu和内存的使用情况等信息。熟悉如何查看它们有助于我们进行相关的统计分析和进一步的操作。

Quote:

$ ps -ef #查看系统所有当前进程的属性
$ ps -C init #查看命令中包含某个指定字符的程序对应的进程,进程ID是1,TTY为?表示和终端没有关联
  PID TTY          TIME CMD
    1 ?        00:00:01 init
$ ps -U falcon #选择某个特定用户启动的进程
$ ps -e -o "%C %c"  #可以按照指定格式输出指定内容,这里会输出命令名和cpu使用率
$ ps -e -o "%C %c" | sort -u -k1 -r | head -5  #这样则会打印cpu使用率最高的前4个程序
 7.5 firefox-bin
 1.1 Xorg
 0.8 scim-panel-gtk
 0.2 scim-bridge
$ ps -e -o "%z %c" | sort -n -k1 -r | head -5  #使用虚拟内存最大的5个进程
349588 firefox-bin
 96612 xfce4-terminal
 88840 xfdesktop
 76332 gedit
 58920 scim-panel-gtk


    由于系统所有进程之间都有“亲缘”关系,所以可以通过pstree查看这种关系,
Quote:

$ pstree #打印系统进程调用树,可以非常清楚地看到当前系统中所有活动进程之间的调用关系


    动态查看进程信息,  
Quote:

$ top


    该命令最大的特点是可以动态的查看进程的信息,当然,它还提供了一些有用的参数,比如-S可以按照累计执行时间的大小排序查看,也可以通过-u查看指定用户启动的进程等。

    感觉有上面几个命令来查看进程的信息就差不多了,下面来讨论一个有趣的问题:如何让一个程序在同一时间只有一个在运行。
    这意味着当一个程序正在被执行时,它将不能再被启动。那该怎么做呢?
    假如一份相同的程序被复制成了很多份,并且具有不同的文件名被放在不同的位置,这个将比较糟糕,所以我们考虑最简单的情况,那就是这份程序在整个系统上是唯一的,而且名字也是唯一的。这样的话,我们有哪些办法来回答上面的问题呢?
    总的机理是:在这个程序的开头检查自己有没有执行,如果执行了则停止否则继续执行后续代码。
    策略则是多样的,由于前面的假设已经保证程序文件名和代码的唯一性,所以
  • 通过ps命令打印找出当前的所有进程对应的程序名,逐个与自己的程序名比较,如果已经有,那么说明自己已经运行了。


    Code:
    ps -e -o "%c" | tr -d " " | grep -q ^init$   #查看当前程序是否执行 [ $? -eq 0 ] && exit           #如果在,那么退出, $?表示上一条指令是否执行成功
    [Ctrl+A Select All]


  • 每次运行时先在指定位置检查是否存在一个保存自己进程ID的文件,如果不存在,那么继续执行,如果存在,那么查看该进程ID是否正在运行,如果在,那么退出,否则往该文件重新写的新的进程ID,并继续。



    Code:
    pidfile=/tmp/$0".pid" if [ -f $pidfile ]; then         OLDPID=$(cat $pidfile)         ps -e -o "%p" | tr -d " " | grep -q "^$OLDPID$"         [ $? -eq 0 ] && exit fi echo $$ > $pidfile #... 代码主题 trap "rm $pidfile"      0      #设置信号0的动作,当程序退出时触发该信号从而删除掉临时文件
    [Ctrl+A Select All]


        更多实现策略自己尽情的发挥吧!

    4、调整进程的优先级

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

    Quote:

    $ ps -e -o "%p %c %n" | grep xfs #打印的信息分别是进程ID,进程对应的程序名,优先级
     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



    5、结束进程

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

    Quote:

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


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

    $ 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信号让它继续运行。

    Quote:

    $ 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命令为我们提供了非常好的功能,不过kill命令只能根据进程的ID或者作业来控制进程,所以pkill和killall给我们提供了更多选择,它们扩展了通过程序名甚至是进程的用户名来控制进程的方法。更多用法请参考它们的手册。

        当一个程序退出以后,如何判断这个程序是正常退出还是异常退出呢?还记得Linux下,那个经典"hello,world"程序吗?在代码的最后总是有条 “return 0”语句。这个“return 0”实际上是为了让程序员来检查进程是否正常退出的。如果进程返回了一个其他的数值,那么我们可以肯定的说这个进程异常退出了,因为它都没有执行到 “return 0”这条语句就退出了。
        那怎么检查进程退出的状态,即那个返回的数值呢?
        在shell程序中,我们可以检查这个特殊的变量$?,它存放了上一条命令执行后的退出状态。

    Quote:

    $ 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,那么他将会很苦恼,会怀疑自己的程序哪个地方出了问题,检查半天却不知所措,因为他太信任你了,竟然从头至尾都没有怀疑你的编程习惯 可能会与众不同。

    6、进程通信

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

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

        如果用多了命令行,这个管子"|"应该会经常用。比如我们在上面的演示中把ps命令的输出作为grep命令的输入,从而可以过滤掉一些我们感兴趣的信息:

    Quote:

    $ ps -ef | grep init



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

       
  • 有名管道(named pipe):

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

    Quote:

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



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

    Quote:

    $ cat app.sh    #应用程序的代码
    #!/bin/bash

    FIFO=fifo_test
    while :;
    do
                    CI=`cat $FIFO`  #CI --> Control Info
                    case $CI in
                                    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放到web的cgi目录下,并对它作一些修改,以使它符合CGI的规范,这些规范包括文档输出 格式的表示(在文件开头需要输出content-tpye: text/html以及一个空白行)和输入参数的获取(web输入参数都存放在QUERY_STRING环境变量里头)。因此一个非常简单的CGI形式控 制程序将类似下面。



    Code:
    #!/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
    [Ctrl+A Select All]



        在实际使用的时候,请确保control.sh能够访问到fifo_test管道,并且有写权限。这样我们在浏览器上就可以这样控制app.sh了。

    http://ipaddress_or_dns/cgi-bin/control.sh?0

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

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

        在C语言中,如果要用有名管道,和shell下的类似,只不过在读写数据的时候用read,write调用,在创建fifo的时候用mkfifo函数调用。

       
  • 信号(Signal):

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

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

    Quote:

    $ function signal_handler {    #定一个signal_handler的函数,>是按下换行符号自动出现的
    > echo "hello, world"
    > }
    $ trap signal_handle SIGINT    #执行该命令设定:当发生SIGINT信号时将打印hello。
    $ hello, world            #按下CTRL+C,可以看到屏幕上输出了hello, world字符串



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

    Quote:

    $ cat sigexit.sh
    #!/bin/bash

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

      

    7、作业和作业控制

        当我们为完成一些复杂的任务而将多个命令通过|,>,<, ;, (, )等组合在一起的时候,通常这样个命令序列会启动多个进程,它们之间通过管道等进行通信。而有些时候,我们在执行一个任务的同时,还有其他的任务需要处 理,那么就经常会在命令序列的最后加上一个&,或者在执行命令以后,按下CTRL+Z让前一个命令暂停。以便做其他的任务。等做完其他一些任务以 后,再通过fg命令把后台的任务切换到前台。这样一种控制过程通常被成为作业控制,而那些命令序列则被成为作业,这个作业可能涉及一个或者多个程序,一个 或者多个进程。下面演示一下几个常用的作业控制操作。

    Quote:

    $ sleep 50 &      #让程序在后台运行,将打印[作业号]和进程号
    [1] 11137
    $ fg %1            #使用shell内置命令fg把作业1调到前台运行,然后按下CTRL+Z让该进程暂停
    sleep 50

    [1]+  Stopped                 sleep 50
    $ jobs            #查看当前作业情况,有一个作业停止
    [1]+  Stopped                 sleep 50
    $ sleep 100 &       #让另外一个作业在后台运行
    [2] 11138        
    $ jobs            #查看当前作业情况,一个正在运行,一个停止
    [1]+  Stopped                 sleep 50
    [2]-  Running                 sleep 100 &
    $ bg %1            #启动停止的进程并让它在后台运行
    [2]+ sleep 50 &



        不过,要在命令行下使用作业控制,需要当前shell,内核终端驱动等对作业控制支持才行。

    参考资料

    <UNIX环境高级编程>相关章节

  • 你可能感兴趣的:(shell编程范例之进程的操作)