unix shell流程_深入研究UNIX流程创建

分配给系统管理员的众多工作之一就是确保用户程序正常运行。 由于存在在系统上同时运行的其他程序,此任务变得更加复杂。 由于各种原因,这些程序可能会失败,挂断或行为异常。 了解UNIX®环境如何创建,管理和销毁这些作业是构建更可靠的系统的关键步骤。

开发人员还具有学习内核如何管理进程的动机,因为在系统其余部分运行良好的应用程序占用的资源更少,并且不会经常激怒系统管理员。 由于创建僵尸进程(稍后描述)而不断重新启动的应用程序显然是不可取的。 对管理过程的UNIX系统调用的理解使开发人员可以编写可以在后台静默运行的软件,而不需要必须保留在某人屏幕上的终端会话。

管理这些程序的基本构件是过程。 进程是操作系统正在执行的程序的名称。 如果您熟悉ps命令,那么您就会熟悉一个过程清单,如清单1所示。

清单1. ps命令的输出
sunbox#ps -ef 
     UID   PID  PPID   C    STIME TTY         TIME CMD
    root     0     0   0 20:15:23 ?           0:14 sched
    root     1     0   0 20:15:24 ?           0:00 /sbin/init
    root     2     0   0 20:15:24 ?           0:00 pageout
    root     3     0   0 20:15:24 ?           0:00 fsflush
  daemon   240     1   0 20:16:37 ?           0:00 /usr/lib/nfs/statd
...

前三列对于此讨论很重要。 第一个列出了进程正在运行的用户,第二个列出了进程的ID,第三个列出了进程的父级的ID。 最后一栏是对过程的描述,通常是启动的二进制文件的名称。 每个进程都分配有一个标识符,称为进程标识符(PID)。 进程还具有父级,在大多数情况下,父级是启动它的进程的PID。

父PID(PPID)的存在意味着一个进程是由另一个进程创建的。 最初的过程,踢这一关叫做init ,它总是给予1的PID init是通过在启动时内核启动的第一个真正的进程。 启动其余的系统是init的工作。 init和其他PPID为0的进程属于内核。

使用fork系统调用

fork(2)系统调用创建一个新进程。 清单2显示了在简单的C代码段中使用的fork

清单2. fork(2)的简单用法
sunbox$ cat fork1.c
#include <unistd.h>
#include <stdio.h>

int main (void) {

        pid_t p; /* fork returns type pid_t */
        p = fork();
        printf("fork returned %d\n", p);
}

sunbox$ gcc fork1.c -o fork1
sunbox$ ./fork1
fork returned 0
fork returned 698

fork1.c的代码只是对fork进行调用,并通过对printf的调用来打印整数结果。 仅发出一个呼叫,但输出被打印两次。 这是因为在fork的调用中创建了一个新进程。 现在,从呼叫返回两个单独的过程。 通常将其描述为“调用一次,返回两次”。

fork返回的值很有趣。 其中之一返回0;否则返回0。 另一个是非零值。 获得0的进程称为子进程 ,非零结果将进入原始进程,即父进程 。 您使用返回值来确定哪个进程是哪个进程。 因为两个进程都在相同的空间恢复执行,所以唯一实际的区别是fork的返回值。

返回值0和非零的基本原理是,子代始终可以通过调用getppid(2)getppid(2)其父代是谁,但是对于父代来说,找到其所有子代更加困难。 因此,父母将被告知其新孩子,如果需要,孩子可以查询其父母。

考虑到fork的返回值,代码现在可以检查它是父进程还是子进程,并采取相应的措施。 清单3显示了一个基于fork结果打印不同输出的程序。

清单3.使用fork的更完整示例
sunbox$ cat fork2.c
#include <unistd.h>
#include <stdio.h>

int main (void) {

        pid_t p;

        printf("Original program, pid=%d\n", getpid());
        p = fork();
        if (p == 0) {
                printf("In child process, pid=%d, ppid=%d\n",
                        getpid(), getppid());
        } else {
                printf("In parent, pid=%d, fork returned=%d\n",
                        getpid(), p);
        }
}

sunbox$ gcc fork2.c -o fork2
sunbox$ ./fork2
Original program, pid=767
In child process, pid=768, ppid=767
In parent, pid=767, fork returned=768

清单3中 ,在每个步骤中都会打印出PID,并且代码检查fork的返回值,以确定哪个进程是父进程,哪个进程是子进程。 比较打印的PID,您可以看到原始进程是父进程(PID 767),而子进程(PID 768)知道其父进程是谁。 注意孩子如何通过getppid认识其父母,以及父母如何使用fork的结果来定位其孩子。

现在您已经了解了复制流程的方法,下面让我们研究如何运行另一个流程。 fork只是方程式的一半。 exec系列系统调用运行实际程序。

使用exec系列系统调用

exec的工作是用新流程替换当前流程。 注意使用单词replace 。 调用exec ,当前进程消失,新进程开始。 如果要创建一个单独的进程,则必须首先fork ,然后在子进程中exec新的二进制文件。 清单4显示了这种情况。

清单4.通过将forkexec配对来运行另一个程序
sunbox$ cat exec1.c
#include <unistd.h>
#include <stdio.h>

int main (void) {

        /* Define a null terminated array of the command to run
           followed by any parameters, in this case none */
        char *arg[] = { "/usr/bin/ls", 0 };

        /* fork, and exec within child process */
        if (fork() == 0) {
                printf("In child process:\n");
                execv(arg[0], arg);
                printf("I will never be called\n");
        }
        printf("Execution continues in parent process\n");
}
sunbox$ gcc exec1.c -o exec1
sunbox$ ./exec1
In child process:
fork1.c      exec1        fork2       exec1.c      fork1
fork2.c      
Execution continues in parent process

清单4中的代码首先定义一个数组,第一个元素是将要执行的二进制文件的路径,其余元素充当命令行参数。 数组在手册页中以null终止。 从返回后fork的系统调用,子进程被指示execv新的二进制文件。

execv首先使用一个指向要运行的二进制文件名称的指针,然后是一个指向您先前声明的参数数组的指针。 数组的第一个元素实际上是二进制文件的名称,因此它实际上是参数开始的第二个元素。 请注意,子进程从不会从对execv的调用返回。 这表明正在运行的进程已被新进程替换。

还有其他exec过程的系统调用,它们的区别在于接受参数的方式以及是否需要传递环境变量。 execv(2)是替换当前映像的较简单方法之一,因为它不需要有关环境的信息,并且使用以空值结尾的数组。 其他选项是execl(2)execvp(2) ,它们分别使用各个参数中的参数,而execvp(2)也采用以空值终止的环境变量数组。 更复杂的是,并非所有操作系统都支持所有变体。 使用哪种决定取决于平台,编码样式以及是否需要定义任何环境变量。

调用fork时打开文件会怎样?

当进程复制自身时,内核会复制所有打开的文件描述符。 文件描述符是一个整数,它表示打开的文件或设备,用于读取和写入。 如果程序在fork之前打开了文件,则如果两个进程都尝试读或写,会发生什么情况? 一个进程会覆盖另一个进程的数据吗? 是否将读取文件的两个副本? 清单5通过打开两个文件(一个用于读取和一个用于写入)并同时使父级和子级同时读写来研究此问题。

清单5.两个进程同时读取和写入同一文件
#include <stdio.h>
#include <strings.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(void) {

        int fd_in, fd_out;
        char buf[1024];

        memset(buf, 0, 1024); /* clear buffer*/
        fd_in = open("/tmp/infile", O_RDONLY);
        fd_out = open("/tmp/outfile", O_WRONLY|O_CREAT);

        fork(); /* It doesn't matter about child vs parent */

        while (read(fd_in, buf, 2) > 0) { /* Loop through the infile */
                printf("%d: %s", getpid(), buf);
                /* Write a line */
                sprintf(buf, "%d Hello, world!\n\r", getpid());
                write(fd_out, buf, strlen(buf));
                sleep(1);
                memset(buf, 0, 1024); /* clear buffer*/
        }
        sleep(10);
}

sunbox$ gcc fdtest1.c -o fdtest1
sunbox$ ./fdtest1
2875: 1
2874: 2
2875: 3
2874: 4
2875: 5
2874: 6
2874: 7
sunbox$ cat /tmp/outfile
2875 Hello, world!
2874 Hello, world!
2875 Hello, world!
2874 Hello, world!
2875 Hello, world!
2874 Hello, world!
2874 Hello, world!

清单5是一个简单的程序,该程序打开一个文件并将fork插入父级和子级。 每个进程都从相同的文件描述符(只是一个编号为1到7的文本文件)读取,打印与PID一起读取的内容。 读取一行后,将PID写入输出文件。 当in文件中没有其他字符要读取时,循环完成。

清单5的输出显示,当一个进程从文件中读取文件时,两个进程的文件指针都被移动了。 同样,当写入文件时,下一个字符将到达文件末尾。 这是有道理的,因为内核会跟踪打开文件的信息。 文件描述符仅仅是该过程的标识符。

您可能还知道标准输出(屏幕)也是文件描述符。 这在fork期间是重复的,这就是两个进程都可以写入屏幕的原因。

父母或子女死亡

流程必须在某个时候完成。 这只是谁先死的问题:父母还是孩子。

父母先于孩子去世

如果父进程在其子进程之前死亡,则孤儿需要知道谁是其父进程。 回想一下,每个进程都有一个父级,您可以将此排序的族谱树一直追溯到PID 1,否则称为init 。 如清单6所示,当父母去世时, init收养其所有子女。

清单6.父进程在子进程之前死亡
#include <unistd.h>
#include <stdio.h>

int main(void) {

        int i;
        if (fork()) {
                /* Parent */
                sleep(2);
                _exit(0);
        }
        for (i=0; i < 5; i++) {
                printf("My parent is %d\n", getppid());
                sleep(1);
        }
}
sunbox$ gcc die1.c -o die1
sunbox$ ./die1
My parent is 2920
My parent is 2920
sunbox$ My parent is 1
My parent is 1
My parent is 1

在此示例中,父进程调用fork ,等待两秒钟,然后退出。 子进程继续打印其父PID五秒钟。 您可以看到随着父级的去世,PPID变为1。 还需要注意的是shell提示的返回。 由于子进程在后台运行,因此父进程死后,控制权将立即返回到外壳程序。

孩子先于父母去世

清单7显示了与清单6相反的内容-也就是说,孩子在父对象之前死亡。 为了更好地说明正在发生的事情,该过程本身未打印任何内容。 相反,有趣的信息来自过程列表。

清单7.子进程在父进程之前死亡
sunbox$ cat die2.c
#include <unistd.h>
#include <stdio.h>

int main(void) {

        int i;
        if (!fork()) {
                /* Child exits immediately*/
                _exit(0);
        }
	/* Parent waits around for a minute */
        sleep(60);
}

sunbox$ gcc die2.c -o die2
sunbox$ ./die2 &
[1] 2934
sunbox$ ps -ef | grep 2934
    sean  2934  2885   0 21:43:05 pts/1       0:00 ./die2
    sean  2935  2934   0        - ?           0:00 <defunct>
sunbox$ ps -ef | grep 2934
[1]+  Exit 199                ./die2

die2使用&运算符在后台运行,然后显示进程列表,仅显示正在运行的进程及其子进程。 PID 2934是父进程,而PID 2935是fork并立即终止的进程。 尽管子进程不合时宜地退出了,但它仍然是进程表中已失效的进程,也称为僵尸 。 当父母在60秒后去世时,两个过程都消失了。

当子进程死亡时,它的父进程会收到信号SIGCHLD通知。 目前,确切的机制并不重要。 重要的是父母必须以某种方式承认孩子的死亡。 从孩子死亡到父母确认信号的时间,孩子都处于僵尸状态。 僵尸没有运行或没有消耗CPU周期; 它仅占用进程表空间。 当父代去世时,内核最终可以与父代一起获得未确认的子代。 这意味着摆脱僵尸进程的唯一方法是杀死父进程。 应对僵尸的最佳方法是确保它们首先不会发生。 清单8中的代码实现了一个信号处理程序来处理传入的SIGCHLD信号。

清单8.运行中的信号处理程序
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>

void sighandler(int sig) {
        printf("In signal handler for signal %d\n", sig);
        /* wait() is the key to acknowledging the SIGCHLD */
        wait(0);
}

int main(void) {

        int i;
        /* Assign a signal handler to SIGCHLD */
        sigset(SIGCHLD, &sighandler);
        if (!fork()) {
                /* Child */
                _exit(0);
        }
        sleep(60);
}
sunbox$ gcc die3.c -o die3
sunbox$ ./die3 &
[1] 3116
sunbox$ In signal handler for signal 18
ps -ef | grep 3116
    sean  3116  2885   0 22:37:26 pts/1       0:00 ./die3

由于sigset函数将清单8的功能指针分配给了信号处理程序,因此清单8比前一个示例稍微复杂一些。 只要进程接收到已处理的信号,就会调用通过sigset分配的函数。 对于SIGCHLD信号,应用程序必须调用wait(3c)函数来等待子进程退出。 由于该进程已经退出,因此可以将孩子的死亡确认为内核。 实际上,父母可能要做的不仅仅是简单地确认信号。 它还可能需要清理孩子的数据。

在执行die3 ,将检查进程列表,并且子进程将干净地执行。 调用信号处理程序的值为18( SIGCHLD ),确认孩子的退出,并且父级返回其sleep(60)

摘要

UNIX进程是在一个进程调用fork时创建的,该进程将正在运行的可执行文件拆分为两个。 然后,该进程可以执行exec系列中的一个系统调用,该系列将新的映像替换为当前的运行映像。

当父进程死亡时,其所有子进程都由init ,它是PID1。如果子进程在父进程之前死亡,则会向父进程发送信号,然后子进程进入僵尸状态,直到确认该信号为止,或父进程被杀死。

既然您了解了如何创建和销毁进程,那么您将能够更好地处理正在运行系统的进程,尤其是那些大量使用多个进程(例如Apache)的进程。 如果需要进行一些故障排除,能够遵循特定过程的过程树还可以使您将任何应用程序追溯到创建它的过程。


翻译自: https://www.ibm.com/developerworks/aix/library/au-unixprocess.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值