进程控制

本文详细介绍了进程控制的基础概念,包括进程的表示、fork函数的工作原理、vfork函数的特点、进程的终止与等待、竞态条件的处理、exec函数的使用、解释器文件的概念以及system函数的实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

转自:blog.youkuaiyun.com/minpro


进程控制

1、   进程表示

某些专用进程:

进程 ID 0 (调度进程 / 交换进程 swapper ):该进程并不执行任何磁盘上的程序——它是内核 的一部分,因此也被称为系统进程 。

进程 ID 1 ( init 进程 ):在自举过程结束时由内核调用。该进程的程序文件在 UNIX 的早期版本中是 etc/init ,在较新版本中是 sbin/init 。此进程负责在内核自举后启动一个 UNIX 系统。 Init 通常读与系统有关的初始化文件(/etc/rc* 文件),并将系统引导到一个状态(例如多用户)。 Init 进程决不会终止。是一个普通的用户进程 ,但是它以超级用户特权运行。

进程 ID 2 :在某些 UNIX 的虚存实现中,进城 ID 2 是页精灵进程 ( pagedaemon )。此进程负责支持虚存系统的请页操作。它是内核进程 。

除了进程 ID ,每个进程还有一些其它标识符。下列函数返回这些标识符:

#include <sys/types.h>

#include <unistd.h>

pid_t  getpid( void );   // 返回:调用进程的进程 ID

pid_t  getppid( void );   // 返回:调用进程的父进程 ID

uid_t  getuid( void );   // 返回:调用进程的实际用户 ID

uid_t  geteuid( void );   // 返回:调用进程的有效用户 ID

gid_t  getgid( void );   // 返回:调用进程的实际组 ID

gid_t  getegid( void );   // 返回:调用进程的有效组 ID

 

2、   fork 函数

父、子进程:

一个现存进程调用 fork 函数是 UNIX 内核创建一个新进程的唯一方法(以上的 0/1/2 进程除外,这些进程是由内核作为自举过程的一部分以特殊方式创建的)。

#include <sys/types.h>

#include <unistd.h>

pid_t  fork( void );   // 返回:子进程中为 0 ,父进程中为子进程 ID ,出错为 -1

(该函数被调用一次,但返回两次)

子进程和父进程继续执行 fork 之后的指令。子进程是父进程的复制品。例如,子进程获得父进程数据空间、堆和栈的复制品。注意:这是子进程所拥有的拷贝。父、子进程并不共享这些存储空间部分。如果正文段是只读的,则父、子进程共享正文段。

现在很多的实现并不做一个父进程数据段和堆的完全拷贝,因为在 fork 之后经常跟随着 exec 。作为替代,使用了在写时复制( Copy-On-Write , COW ) 的技术。这些区域由父子进程共享,而且内核将它们的存取许可权改变为只读 的。如果有进程试图修改这些区域,则内核为有关部分,典型的是虚存系统中的“页”,做一个拷贝。

文件共享:

fork 的一个特性是所有由父进程打开的描述符都被复制到子进程中。父、子进程每个相同的打开描述符共享一个文件表项。(文件表项:文件状态标志 / 当前文件位移 /v 节点指针)。这种共享文件的方式使父、子进程对同一文件使用了一个文件位移量。达到同步。

在 fork 之后处理文件描述符有两种常见的情况:

1)         父进程等待子进程完成。此时,父进程无需对其描述符做任何处理,当子进程终止后,它曾进行过读、写操作的人一共享描述符的文件位移量已做了相应更新。

2)         父、子进程各自执行不同的程序段。此时,在 fork 之后,父、子进程各自关闭它们不需使用的文件描述符,并且不干扰对方使用的文件描述符。这种方法是网络服务进程中经常使用的。

使 fork 失败的两个主要原因是:

1)         系统中已经有了太多的进程(通常意味着某个方面除了问题)

2)         该实际用户 ID 的进程总数超过了系统限制。( CHILD_MAX 规定了每个实际用户 ID 在任一时刻可具有的最大进程数)

fork 有两种用法:

1)         一个父进程复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程中常见——父进程等待委托者的服务请求。当这种请求到达时,父进程调用 fork ,使子进程处理此请求。父进程则继续等待下一个服务请求。

2)         一个进程要执行一个不同的程序。这对 shell 是常见的情况。此时,子进程在从 fork 返回后立即调用 exec。

附注:某些操作系统将 2 )中的两个操作 fork/exec 组合成一个,并称其为 spawn 。 UNIX 将这两个操作分开,因为很多场合需要单独使用 fork ,而并不跟随 exec 。另外,将这两个操作分开,使得子进程在 fork 和 exec 之间可以更改自己的属性。

3、   vfork 函数

vfork 函数用于创建一个新进程,而该新进程的目的是 exec 一个新程序。 vfork 并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用 exec( 或 exit) ,也就不会存访该地址空间。不过在子进程调用 exec 或 exit 之前,它在父进程的空间中运行。这种工作方式在某些 UNIX 的页式虚存实现中提高了效率。

与 fork 的另一个区别是: vfork 保证子进程先运行,在它调用 exec 或 exit 之后父进程才可能被调度运行。(如果再调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁)。

不常用,简单了解。

4、   exit 函数(进程的终止)

如果子进程正常终止,可以通过退出状态( exit status )参数来使父进程获得子进程的退出状态。如果子进程异常中止,其父进程可以用 wait 或 waitpid 函数取得其终止状态。

如果父进程在子进程之前终止 ,则子进程将由 init 进程领养 。其操作过程大致是:在一个进程终止时,内核逐个检查所有活动进程,以判断它是否是正要终止的进程的子进程,如果是,则该进程的父进程 ID 就更改为 1 ( init进程 ID )。这保证了每个进程都有一个父进程。

如果子进程在父进程之前终止 ,父进程是如何得到子进程的终止状态呢?——内核为每个终止子进程保存了一定量的信息,所以当终止进程的父进程调用 wait 或 waitpid 时,可以得到有关信息。这种信息至少包括进程 ID 、该进程的终止状态、以及该进程使用的 CPU 时间总量。(一个已经终止,但其父进程尚未对其进行善后处理(获取终止子进程的有关信息、释放它仍占用的资源)的进程成为 僵死进程( zombie ) 。 ps(1) 命令将僵死进程的状态打印为 Z )。(进程已经终止,但父进程尚未调用 wait 或者 waitpid 等取其状态,自进程就会变成僵死进程。)

一个由 init 领养的进程终止时, init 就会调用一个 wait 函数取得其终止状态。这防止了在系统中有很多僵死进程。

Exit 和 _exit 函数:

_exit 立即进入内核, exit 则先执行一些清楚处理,(包括调用执行各终止处理程序,关闭所有标准 I/O 流等),然后进入内核。

5、   wait 和 waitpid 函数

   当一个进程正常或异常终止时,内核就向其父进程发送 SIGCHLD 信号。因为子进程终止是个异步事件(这可以在父进程运行的任何时候发生),所以这种信号也是内核向父进程发的异步通知。

调用 wait 或 waitpid 的进程可能会:

阻塞(如果其所有子进程都还在运行)—— wait 不到一个子进程的终止状态

带子进程的终止状态立即返回(如果一个子进程已终止,正等待父进程存取其终止状态)——顺利取到子进程的终止状态,返回

出错立即返回(如果它没有任何子进程)——出错,返回

#include <sys/types.h>

#include <sys/wait.h>

Pid_t wait(int *statloc);    // 返回终止子进程的进程 ID

Pid_t waitpid(pid_t pid, int *statloc, int options);       // 返回终止子进程的进程 ID

Wait:

如果一个进程已经终止,是一个僵死进程,则 wait 立即返回并取得该子进程的状态,否则 wait 使其调用者阻塞知道一个子进程终止。如果调用者阻塞而且它有多个子进程,则在任一子进程终止时, wait 就立即返回。

Statloc 是一个整型指针,或者为空(不关心终止状态),或者返回终止进程的终止状态。

Waitpid:

Waitpid 并不等待第一个终止的子进程,它有一选择项,可使调用者不阻塞,还有若干选择项,可以控制它所等待的进程。

Statloc 参数同 wait 。

Pid 参数:

Pid == -1 等待任一子进程。于是在这一功能方面 waitpid 与 wait 等效。

Pid > 0 等待其进程 ID 与 pid 相等的子进程。

Pid == 0 等待其组 ID 等于调用进程的组 ID 的任一子进程。

Pid < -1 等待其组 ID 等于 pid 的绝对值得任一子进程。

  Options 参数:

或者是0或者下表中常数的逐位或运算。

常数

说明

WNOHANG

若由 pid 指定的子进程并不立即可用,则 waitpid 不阻塞,此时其返回值为 0

WUNTRACED

若某实现支持作业控制,则由 pid 指定的任一子进程状态已暂停,且其状态自暂停以来还未报告过,则返回其状态。 WIFSTOPPED 宏确定返回值是否对应于一个暂停子进程。

  Waitpid 支持作业控制。

如何用 wait 等待一个指定的进程终止:

调用 wait ,然后将其返回的进程 ID 和所期望的进程 ID 比较。如果终止进程不是所期望的,则将该进程 ID 和终止状态保存起来,然后再次调用 wait 。反复直到所期望的进程终止。下一次又想等待一个特定进程时,先查看已终止的进程表,若其中已有要等待的进程,则取有关信息,否则调用 wait 。

Fork 两次以避免僵死进程(不懂)

POSIX.1 规定终止状态用定义在 <sys/wait.h> 中的各个宏来查看。有三个互斥的宏可用来取得进程终止的原因,它们的名字都以 WIF 开始。这三个宏中某一个为真,就可以利用一些其他的宏来取得终止状态、信号编号等。详见下表:

说明

WIFEXITED(status)

若为正常终止子进程返回的状态,则为真。对于这种情况可执行WEXITSTATUS(status) 取子进程传送给 exit 或 _exit 参数的低 8 位

WIFSIGNALED(status)

若为异常终止子进程返回的状态,则为真(接到一个不捕捉的信号)。对于这种情况可执行 WTERMSIG(status) 取使子进程终止的信号编号。

另外, SVR4 和 4.3+BSD (但是,非 POSIX.1 )定义宏:

WCOREDUMP(status) ,若已产生终止进程的 core 文件,则它返回真

WIFSTOPPED(status)

若为当前暂停子进程的返回的状态,则为真。对于这种情况,可执行WSTOPSIG(status) 取使子进程暂停的信号编号

例:使用以上宏打印进程的终止状态。

#include <sys/types.h>

#include <sys/wait.h>

#include <stdio.h>

 

void

pr_exit(int status)

{

    if(WIFEXITED(status))  // 如果是正常终止,使用 WEXITSTATUS 取状态

        printf("normal termination, exit status = %d/n", WEXITSTATUS(status));

    else

        if(WIFSIGNALED(status))   // 如果是异常终止

        {

            printf("abnormal termination, signal number = %d%s/n",

                WTERMSIG(status),    // 取进程终止的信号编号

          #ifdef WCOREDUMP

                WCOREDUMP(status)? "(core file generated)" : "" );

          #else

                "");

          #endif

        }

        else

            if(WIFSTOPPED(status))    // 如果是暂停

                printf("child stopped, signal number = %d/n", WSTOPSIG(status));

}

测试以上程序:

int main(void)

{

    pid_t pid;

    int status;

    if( (pid = fork()) < 0 )

        printf("fork error");

    else if( pid == 0)

        exit(7);

       

    if(wait(&status) != pid)    /*wait for child*/

        printf("wait error");

    pr_exit(status);            /*and print its status*/ // 打印正常终止

   

     if( (pid = fork()) < 0)

        printf("fork error");

    else if( pid == 0)          /*chid*/

        abort();                /*generates SIGABRT*/ 

//abort 函数用于异常终止一个进程,并发送 SIGABRT 信号

       

    if(wait(&status) != pid)    /*wait for child*/

        printf("wait error");

    pr_exit(status);            /*and print its status*/

   

    if( (pid = fork()) < 0)

        printf("fork error");

    else if( pid == 0)          /*child*/ 

        status /= 0;            /*divide by 0 generates SIGFPE*/

           

    if(wait(&status) != pid)    /*wait for child*/

        printf("wait error");

    pr_exit(status);            /*and print its status*/

   

    exit(0);

}

结果:

> printexit.exe

normal termination, exit status = 7

abnormal termination, signal number = 6(core file generated)

abnormal termination, signal number = 8(core file generated)

>  

 

6、   竞态条件

当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,则认为发生了竞态条件(race condition )。

如果一个进程希望等待一个子进程终止,则它必须调用 wait 函数。如果一个进程要等待其父进程终止,则可使用下列形式的循环:

While( getpid() != 1 )

   Sleep(1);

这种形式的循环(称为定期询问( polling )),浪费了 CPU 时间,因为调用者每隔 1 秒都被唤醒,然后进行条件测试。

为了避免竞态条件和定期询问,在多个进程之间需要有某种形式的信号机制。

( fork() 函数的返回:在子进程中为 0 ,父进程中为子进程 ID ,出错为 -1 )

例:具有竞态条件的程序

#include <sys/types.h>

#include <stdio.h>

static void

charatatime(char *str)

{

    char *ptr;

    char c;

    ptr = str;

    setbuf( stdout, NULL );  

    for(c=*ptr; c=*ptr++;)

        putc(c, stdout);

}

以上函数中将标准输出设置为不带缓存的,于是每个字符输出都需调用一次 write 。

这样可以使内核尽可能多次地在两个进程之间进行切换,以例示竞态条件。

int

main(void)

{

    pid_t pid;

    if( (pid = fork()) < 0)  

        printf("fork error");   // 出错

    else

        if( pid == 0)    // 在子进程中返回

        {

            charatatime("output from child /n");

        }

        Else     // 在父进程中返回

         {

            charatatime("output from parent /n");

        }      

        exit(0);

}

结果:

> fork.exe

output from child

output from parent

> fork.exe

outputo utpuft from child

rom parent

7、   Exec 函数

用 fork 可以创建新的进程,用 exec 可以执行新的程序, exit 函数可以处理终止, wait 可以等待终止,这些是我们需要的基本的进程控制原语。

当进程调用一种 exec 函数时,该进程完全由新程序代换,而新程序则从其 main 函数开始执行。因为调用 exec 并不创建新进程,所以前后的进程 ID 并未改变。 Exec 只是用另一个新程序替换了当前进程的正文、数据、堆和栈段。

六种不同的 exec 函数如下:

#include <unistd.h>

Int execl( const char *pathname, const char *arg0, … /* ( char *)0 */ );

Int execv( const char *pathname, char *const argv[] );

Int execle(const char *pathname, const char *arg0, … /* ( char *)0, char *const envp[] */ );

Int execve( const char *pathname, char *const argv[],char *const envp[] );

Int execlp( const char *filename, const char *arg0, … /* ( char *)0 */ );

Int execvp( const char *filename, char *const argv[] );

参数:

p :表示该函数取 file 那么作为参数,并且用 PATH 环境变量寻找可执行文件。

l :( list )表示该函数取一个参数表,与 v 互斥。将新程序的每个命令行参数都说明为一个单独的参数。参数表以空指针结尾。

v :( vector )表示该函数取一个 argv[] 。先构造一个指向各参数的指针数组,然后将该数组地址作为着三个函数的参数。

e :表示该函数取 envp[] 数组,而不使用当前环境。传递一个指向环境字符串指针数组的指针。否则使用调用进程中的 environ 变量为新程序复制现存的环境。

filename 参数:如果 filename 中包含 / ,则视为路径名。否则按 PATH 环境变量,在有关目录中搜寻可执行文件。如果找到了一个可执行文件,但不是由连接编辑程序产生的机器可执行代码文件,则认为是一个 shell 脚本,于是试着调用 /bin/sh ,并以该 filename 作为 shell 的输入。

8、   解释器文件    

#!  Pathname  [optional-argument]

Pathname 是个绝对路径名

对这种文件的识别是由内核作为 exec 系统调用处理的一部分来完成的。内核使调用 exec 函数的进程实际执行的文件并不是该解释器文件,而是在该解释器文件的第一行中 pathname 所指定的文件。

解释器文件:文本文件,以 #! 开头

解释器:由该解释器文件第一行中的 pathname 指定

很多系统对解释器文件第一行有常读限制( 2 个字符)。

示例: #! /bin/sh

解释器 / 解释器文件 / 执行解释器文件的程序:

解释器:(回送每一个命令行参数)

///home/liuna/test/printargv/printargv.exe 是一个解释器,即一个可执行程序

#include <stdio.h>

int

main(int argc, char *argv[])

{

    int i;

    for(i=0; i<argc; i++)

    {

        printf("argv[%d]: %s/n", i, argv[i]);

    }

    exit(0);

}

// 当执行一个程序时,调用 exec 的进程可将命令行参数传递给该信程序。

 

解释器文件:

///home/liuna/test/awk/awk.c   就是一个执行解释器的脚本文件。如果用 awk 做解释器,那么解释器文件的第一行典型如下: #! /bin/awk

#!  /home/liuna/test/printargv/printargv.exe   foo  

 

执行解释器文件的程序:

我们编写一个程序来执行解释器文件,并传递一些参数:

///home/liuna/test/execawk/execawk..exe

#include <sys/types.h>

#include <stdio.h>

 

int main(void)

{

    pid_t pid;

    if((pid = fork()) < 0)

        printf("fork error");

    else

        if(pid == 0)

        {

            if(execl("/home/liuna/test/awk/awk.c",

                     "awk", "my argv1", "MY ARGV2", (char *)0 ) < 0)

                printf("execl error");

        }

    if( waitpid(pid, NULL, 0) < 0)

         printf("wait error");

    exit(0);

}

执行以上可执行程序,得到如下结果:

> execawk.exe

argv[0]: /home/liuna/test/printargv/printargv.exe

argv[1]: foo

argv[2]: /home/liuna/test/awk/awk.c

argv[3]: my argv1

argv[4]: MY ARGV2

解析:当内核 exec 该解释器( printargv.exe )时, argv[0] 是该解释器的 pathname , argv[1] 是解释器文件( awk.c)中的可选参数 foo ,其余参数是 pathname ( /home/liuna/test/awk/awk.c ) ,以及第二个和第三个参数( my argv1和 MY ARGV2 )。注意:内核取 execl 中的 pathname 代替第一个参数( awk ),因为一般 pathname 包含了较第一个参数更多的信息。( 什么意思???)     

效率 / 开销 / 原理,都还不清楚!!!

9、   system 函数

#include <stdlib.h>

Int system( const char *cmdstring);

如果 cmdstring 是一个空指针,则仅当命令处理程序可用时, system 返回非 0 值,这一特征可决定在一个给定的操作系统上是否支持 system 函数。在 UNIX 中, system 总是可用的。

因为 system 在其实现中调用了 fork 、 exec 和 waitpid ,因此有三种返回值:

如果 fork 失败或者 waitpid 返回除 EINTR 之外的出错,则 system 返回 -1 ,而且 errno 中设置了错误类型。

如果 exec 失败(表示不能执行 shell ),则其返回值如同 shell 执行了 exit(127) 一样。

否则所有三个函数都成功,并且 system 的返回值是 shell 的终止状态,其格式已在 waitpid 中说明。

 

以下是 system 函数的一种实现,它对信号没有进行处理:

#include <sys/types.h>

#include <sys/wait.h>

#include <errno.h>

#include <unistd.h>

 

// 这仅仅是 system 的一种实现方式,其实 system 是一个系统调用,用来执行命令字符串

// 所以,下面这个 system 函数完全可以改成另一个名字。

int

system(const char *cmdstring)  /*version without signal handling*/

{

    pid_t  pid;

    int  status;

    if(cmdstring == NULL)

        return(-1);

    if((pid = fork()) < 0)

        status = -1;

    else

        if(pid == 0)

        {

            execl("/bin/sh", "sh", "-c", cmdstring, (char *)0 );

            _exit(127);               /*execl error*/

        }

        else

        {

            while( waitpid(pid, &status, 0) < 0)

                 if(errno != EINTR)  

                {

                    status = -1;

                    break;

                }

        }

    return(status);   

}

如果 waitpid 由一个捕捉到的信号中断,则 system 很多当前的实现都返回一个错误( EINTR )。

Shell 的 -c 选项告诉 shell 程序取下一个命令行参数(即 cmdstring )作为命令输入,而不是从标准输入或者从一个给定的文件中读入命令。

如果不使用 shell 执行此命令,而是试图由我们自己去执行,那么将相当困难。首先,我们必须用 execlp 而不是 execl,像 shell 那样使用 PATH 变量。我们必须将 null 符结尾的命令字符串分成各个命令行参数,以便调用 execlp 。最后,我们也不能使用任何一个 shell 元字符。( 不明白 )

注意:我们调用 _exit 而不是 exit 。这是为了防止任一标准 I/O 缓存在子进程中被刷新。(这些缓存会在 fork 中由父进程复制到子进程)( exit 在调用时不是直接进入内核,而是要做一些清理工作)

测试上述 system 的实现:

int

main(void)

{

    int status;

    if( (status = system("date")) < 0)

        printf("system() error");

    pr_exit(status);

   

    if( (status = system("nosuchcommand")) < 0)

        printf("system() error");

    pr_exit(status);

   

    if( (status = system("who; exit 44")) < 0)

        printf("system() error");

pr_exit(status);

 

    exit(0);  

}

结果:

> systemtest.exe

Thu Jan 31 06:56:08 MST 2008

normal termination, exit status = 0

sh: nosuchcommand:  not found.

normal termination, exit status = 127

root       pts/1        Jan 31 19:22

liu      pts/tl       Jan 31 06:50

normal termination, exit status = 44

总结:

这一章有很多东西读不懂了,所以只摘录了一些工作中用到的部分,好多不懂部分以后慢慢体会了。    

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值