进程控制之 fork、wait、exec族函数 及进程资源控制

一,fork

//头文件

#include <unistd.h>

//函数定义

pid_t fork( void );

返回值:子进程中返回0,父进程中返回子进程ID,出错返回-1函数说明:一个现有进程可以调用fork函数创建一个新进程。由fork创建的新进程被称为子进程(child process)。fork函数被调用一次但返回两次。两次返回的唯一区别是子进程中返回0值而父进程中返回子进程ID。子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本。

注意,在fork()的调用处,整个父进程空间会原模原样地复制到子进程中,包括指令,变量值,程序调用栈,环境变量,缓冲区,等等。 子进程持有的是上述存储空间的“副本”,这意味着父子进程间不共享这些存储空间,它们之间共享的存储空间只有代码段
(这里有一个关于fork比较有趣的面试题,  一个fork的面试题

 ,首先来看例1

#include <stdio.h>
#include <unistd.h>

void main()
{
    int i;
    printf("hello, %d\n",getpid());
    i=2;
    fork();
    printf("var %d in %d\n", i, getpid());
}

这是在我的机器上一次执行的结果:

hello, 2808

var 2 in 2808

var 2 in 2809

 

为什么会有两次输出var 2 一行呢?看似不可思议吧…要解释原因,就牵涉到了我们要讨论的fork,它到底做了什么?
fork英文是叉的意思.在这里的意思是进程从这里开始分叉,分成了两个进程,一个是父进程,一个子进程.子进程拷贝
了父进程的绝大部分.栈,缓冲区等等.系统为子进程创建一个新的进程表项,其中进程id与父进程是不相同的,这也就
是说父子进程是两个独立的进程,虽然父子进程共享代码空间.但是在牵涉到写数据时子进程有自己的数据空间,这

是因为copy on write机制,在有数据修改时,系统会为子进程申请新的页面.再来复习下进程的有关知识.系统通过进

程控制块PCB来管理进程.进程的执行,可以看作是在它的上下文中执行.一个进程的上下文(context)由三部分组成:

用户级上下文,寄存器上下文和系统级上下文.用户级上下文中有正文,数据,用户栈和共享存储区;寄存器上下文中有

个非常重要的程序计数器(传说中的)PC,还有栈指针和通用寄存器等;系统级上下文分静态和动态,PCB中进程表项,

U区,还有本进程的表项,页表,系统区表项等都属于静态部分,而核心栈等则属于动态部分.

回到fork上来.fork在内核中对应的是do_fork函数.详见:内核 do_fork

函数源代码浅析(http://bbs.chinaunix.net/thread-2011594-1-1.html). 上面已经提到,fork后,子进程拷贝了父进程

的进程表项,还有栈,缓冲区,U区等等.当然在这之前会去检查系统有没有可用的资源,取一个空闲的进程表项和唯

一的PID号等工作.(后面的例子会体现子进程到底拷贝了父进程的哪些东西.)需要指出的是,这里所说的拷贝,并不

是说子进程再申请页面,将父进程中的全部拷贝过来.而是,他们共享一个空间,子进程只是作一层映射而已,这个时

候进程页面标记为只读.在有数据修改时,才会申请新的页面,拷贝过来,并标记为可写.fork执行后,对父进程和子进

程不同的地方还有,对父进程返回子进程的pid号,对子进程返回的是0.大致的算法描述为:
    if (当前正在执行的是父进程)

{

      将子进程的状态设置为”就绪状态”;

      return (子进程的pid号); }

else

{

      /*正在执行的是子进程*/

     初始化U区等工作;

     return 0;

}
现在来看例1,是不是已经清晰了很多? 在执行了fork之后,父子进程分别都执行了下一步printf语句.由于fork拷贝走

pc,所以在子进程中不会再从main入口重新执行,而是执行fork后的下一条指令.而i是保存在进程栈空间中的,所以

子进程中也存在. 有了前面的基础,再看下面一个例2:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
void main()
{
    int i=0;
    pid_t fork_result;
    printf("pid:%d->main begin()\n",getpid());
    fork_result=fork();
    if(fork_result<0)
    {   
        printf("fork fail\n");
        exit(1);
    }   
    for(i=0; i<3; i++)
    {   
        if(fork_result==0)
            printf("in ID %d child process: %d\n", getpid(), i);
        else
            printf("in ID %d parent process: %d\n", getpid(), i);
    }   
}


这次输出可以更明确的显示出子进程到底拷贝了些什么.我机器上的两次执行结果:
pid:3881->main begin()
in ID 3881 parent process: 0
in ID 3881 parent process: 1
in ID 3881 parent process: 2
in ID 3882 child process: 0
in ID 3882 child process: 1
in ID 3882 child process: 2

pid:3881->main begin()

in ID 3882 child process: 0
in ID 3882 child process: 1
in ID 3882 child process: 2
in ID 3881 parent process: 0
in ID 3881 parent process: 1
in ID 3881 parent process: 2
同时也可以说明,父子进程到底哪个先执行,是跟cpu调度有关系的.如果想固定顺序,那么就要用wait或vfork函数.
继续看例3:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
 
void main()
{
        printf("hello world %d",getpid());
        //fflush(0);
        fork();
}

执行上面的程序,可以发现输出了两遍hello world, hello world 3929hello world 3929.而且两次的pid号都是一样

的.这是为什么呢? 这其实是因为printf的行缓冲的问题,printf语句执行后,系统将字符串放在了缓冲区内,并没有输

出到stdout.不明白的话看下面的例子:

#include <stdio.h>
int main(){
    printf("hello world");
    while(1);
    return 0;
}

执行上面的程序你会发现,程序陷入死循环,并没有输出”hello world”.这就是因为把”hello world”放入了缓冲区.我们平常加’\n’的话,
就会刷新缓冲区,那样就会直接输出到stdout了.因为子进程将这些缓冲也拷贝走了,所以子进程也打印了一遍.父进程直到最后才输

出.他们的输出是一样的,输出的pid是一致的,因为子进程拷贝走的是printf语句执行后的结果.如果利用setbuf设置下,或者在printf语

句后调用fflush(0);强制刷新缓冲区,就不会有这个问题了.这个例子从侧面显示出子进程也拷贝了父进程的缓冲区.关于fork的应用还

很多很多,在实际项目中需要了再去深入研究.关于fork和exec的区别,exec是将本进程的映像给替换掉了,跟fork差别还是很大的,其

实fork创建子进程后,大部分情况下,子进程会调用exec去执行不同的程序的. 


二、vfork()

pid_t vfork( void );

vfork与fork主要有三点区别:

.fork():子进程拷贝父进程的数据段,堆栈段;vfork():子进程与父进程共享数据段.fork()父子进程的执行次序不确定。

vfork 保证子进程先运行,在调用 exec 或 exit 之前与父进程数据是共享的(即子进程在调用exec或exit 之前。它在父

进程的空间中运行),在它调用 exec或 exit 之后父进程才可能被调度运行。vfork()保证子进程先运行,在它调用 exec

或 exit 之后父进程才可能被调度运行.如果在调用这两个函数之前子进程依赖于父进程

的进一步动作,则会导致死锁。

#include <unistd.h>
#include <stdio.h>
int main(void)
{
pid_t pid;
int count=0;
pid=vfork();
count++;
printf("count= %d\n",count);
return 0;
}

执行结果:
./test
count= 1
count= 1
Segmentation fault (core dumped)

分析:
通过将fork()换成vfork(),由于vfork()是共享数据段,为什么结果不是2呢,答案是:
**vfork保证子进程先运行,在它调用 exec 或 exit 之后父进程才可能被调度运行.如果在调用这两个函数之前子进程依赖于父进程的进

一步动作,则会导致死锁.

3)做最后的修改,在子进程执行时,调用_exit(),程序如下:

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
int main(void)
{
pid_t pid;
int count=0;

pid=vfork();

if(pid==0)
{
   count++;
_exit(0);
}
else
{
count++;
}
printf("count= %d\n",count);

return 0;
}

执行结果:
./test
count= 2

分析:如果子进程中如果没有调用_exit(0),则父进程不可能被执行,在子进程调用exec(),exit()之后父进程才可能被调用.
所以加上_exit(0),使子进程退出,父进程执行.
这样 else 后的语句就会被父进程执行,又因在子进程调用 exec 或 exit 之前与父进程数据是共享的,
所以子进程退出后把父进程的数据段 count 改成1了,子进程退出后,父进程又执行,最终就将count 变成了 2.

简要的概括的说是:
1)fork()系统调用是创建一个新进程的首选方式,fork的返回值要么是0,要么是非0,父进程与子进程的根本区别在于fork函数的返回值.
2)vfork()系统调用除了能保证用户空间内存不会被复制之外,它与fork几乎是完全相同的.vfork存在的问题是它要求子进程立即调用exec,
而不用修改任何内存,这在真正实现的时候要困难的多,尤其是考虑到exec调用有可能失败.
3)vfork()的出现是为了解决当初fork()浪费用户空间内存的问题,因为在fork()后,很有可能去执行exec(),vfork()的思想就是取消这种复制.
4)现在的所有unix变量都使用一种写拷贝的技术(copy on write),它使得一个普通的fork调用非常类似于vfork.因此vfork变得没有必要.


 三,wait 和 waitpid 函数

  首先说明子进程与父进程先后终止产生的问题:

 1,如果父进程在子进程终止之前终止,对于父进程已经终止的所有子进程,他们的父进程都改为 init进程(pid为1) 我们称这些子进程

      由init进程领养。

2,如果子进程在父进程之前终止,内核为每个终止的子进程保存了一定量的信息,所以当终止进程的父进程 调用wait 和 waitpid 时,

      可以得到这些信息(包含了进程ID、该进程的终止状态、以及该进程使用的CPU时间总量等)。

       对于一个已经终止、但是父进程尚未对其进行善后处理(获取终止子进程的有关信息并释放它占用的资源) 的进程被称为僵尸进程

    (APUE P182中fork两次避免僵死进程,比较有意思)。

我们一般用wait & waitpid  获得子进程终止状态。 

wait的函数原型是:  
#include<sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status)  
参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。进程一旦调用了wait,就立即阻塞自己,由wait自动分

析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程, wait就会收集这个子进程的信息,并把它彻底

销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
pid_t waitpid(pid_t pid,int *status,int options)

waitpid多出了两个可由用户控制的参数pid和options.,下面介绍这两个参数:
从参数的名字pid和类型pid_t中就可以看出,这里需要的是一个进程ID。但当pid取不同的值时,在这里有不同的意义。   

 pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一

直等下去。    

 pid= -1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。     

pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。    

 pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。

options   options提供了一些额外的选项来控制waitpid,
目前在Linux中只支持WNOHANG和WUNTRACED两个选项, option 可以为 0 或可以用"|"运算符把它们连接起来使用。

 WNOHANG 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若结束,则返回该子进程的ID。
 WUNTRACED 若子进程进入暂停状态,则马上返回,但子进程的结束状态不予以理会。WIFSTOPPED(status)宏确定返回值是否对应与一个

  暂停子进程。

例如:在几乎同一时刻,有N个client 的 FIN发向服务器,同样的,N个SIGCHLD信号到达服务器,然而,UNIX的信号往往是不会排队的,显然这样一来,
信号处理函数将只会执行一次,残留剩余N-1个子进程作为僵尸进程驻留在内核空间。此时,正确的解决办法是利用waitpid(-1, &stat, WNOHANG)

防止留下僵尸进程。其中的pid为-1表明等待任一个终止的子进程,而WNOHANG选择项通知内核在没有已终止进程项时不要阻塞

wait&waitpid 区别 :

waitpid提供了wait函数不能实现的3个功能: 

 1,waitpid等待特定的子进程, 而wait则返回任一终止状态的子进程; 

 2,waitpid提供了一个wait的非阻塞版本(waitpid的 WNOHANG选项); 

 3, waitpid支持作业控制(以WUNTRACED选项). 用于检查wait和waitpid两个函数返回终止状态的宏: 这两个函数返回的子进程状态都保存在statloc指针中, 

用以下3个宏可以检查该状态: 

WIFEXITED(status): 若为正常终止, 则为真. 此时可执行 WEXITSTATUS(status): 取子进程传送给exit或_exit参数的低8位. 

WIFSIGNALED(status): 若为异常终止, 则为真.此时可执行 WTERMSIG(status): 取使子进程终止的信号编号.
WIFSTOPPED(status): 若为当前暂停子进程, 则为真. 此时可执行 WSTOPSIG(status): 取使子进程暂停的信号编号.


四,exec家族


一个fork的面试题

1.exec家族一共有六个函数,分别是:

(1)int execl(const char *path, const char *arg, ......);

(2)int execle(const char *path, const char *arg, ...... , char * const envp[]);

(3)int execv(const char *path, char *const argv[]);

(4)int execve(const char *path, char *const argv[], char *const envp[]);

(5)int execvp(const char *file, char * const argv[]);

(6)int execlp(const char *file, const char *arg, ......);

其中只有execve是真正意义上的系统调用,其它都是在此基础上经过包装的库函数。

    exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行

一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。与一般情况不同,exec函数

族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程ID等一

些表面上的信息仍保持原样,颇有些神似"三十六计"中的"金蝉脱壳"。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失

败了,它们才会返回一个-1,从原程序的调用点接着往下执行。

2.它们之间的区别:

第一个区别是:

前四个取路径名做为参数,后两个取文件名做为参数,如果文件名中不包含“/”则从PATH环境变量中搜寻可执行文件,

如果找到了一个可执行文件,但是该文件不是连接编辑程序产生的可执行代码文件,则当做shell脚本处理。

第二个区别:

前两个和最后一个函数中都包括“ l ”这个字母,而另三个都包括“ v”, " l "代表 list即表,而" v "代表 vector即矢量,

也是是前三个函数的参数都是以list的形式给出的,但最后要加一个空指针,如果用常数0来表示空指针,则必须将

它强行转换成字符指针,否则有可能出错。,而后三个都是以矢量的形式给出,即数组。

最后一个区别:

与向新程序传递环境变量有关,如第二个和第四个以e结尾的函数,可以向函数传递一个指向环境字符串指针数组的

指针。即自个定义各个环境变量,而其它四个则使用进程中的环境变量。

 3.实例讲解:

1)在平时的编程中,如果用到了exec函数族,一定记得要加错误判断语句。先判断execl的返回值,如果出错,可以用perror( )函数

打印出错误信息。

如:if (execl(path,..””(char *)0) < 0)

    {

       perror(“execl error!”);

   }

如果调用出错,可输出:execl error!: 错误原因  这样可方便查找出错原因

2)注意下面书写格式:

先定义一个指针数组:char *argv[]={ls,-l,(char *)0}

execv调用ls:    execv(/bin/ls,argv)

 如果用execvp

execvp(ls,argv)      //直接写ls就可以了

注意:

execl调用 shell时,要在 shell脚本中指明使用的 shell版本: #/bin/bash。在命令行下执行 shell脚本,系统为它自动打开一个 shell,在程序中

没有shell,在调用shell脚本时,会出错,所以要在shell脚本中先打开shell

int execl(const char *path, const char *arg, ...);
execl()用来执行参数path字符串所代表的文件路径, 接下来的参数代表执行该文件时传递的argv[0],argv[1].....是后一个参数必须用空指针NULL作结束

五,在linux中,fork一个子进程,怎么样控制它的运行时间以及占用内存

 首先,我们fork出一个子进程后,父进程与子进程并行执行,我们可以用wait系列函数对子进程进行等待,并用结构体去记录子进程各类资源的使用状况。其中我们用到了struct rusage中的ru_utime和ru_stime,前者是使用的用户时间,后者是系统时间,不过后面那个似乎没怎么用。当然,在运行过程中,如果子进程程序执行时有错误产生,我们可以使用WIFEXITED(stutas) 这个宏来判断父进程等待后子进程是否为正常结束。
这么一个程序框架。
    pid = vfork();//用vfork可以保证子进程比父进程先运行
    if(pid==0)
    {
        //这里进行进程限制和进程输入输出重定向。

        //exec族函数

        exit(1);

    }
    wait(&status);//父进程等待子进程运行
    if(!WIFEXITED(status))
    {
       //处理程序非正常结束状态
    }

status:指向子进程的返回状态,可通过以下宏进行检索

WIFEXITED(status) //返回真如果子进程正常终止,例如:通过调用exit(),_exit(),或者从main()的return语句返回。
WEXITSTATUS(status) //返回子进程的退出状态。这应来自子进程调用exit()或_exit()时指定的参数,或者来自main内部return语句参数的最低字节。只有WIFEXITED返回真时,才应该使用。
WIFSIGNALED(status) //返回真如果子进程由信号所终止
WTERMSIG(status) //返回导致子进程终止的信号数量。只有WIFSIGNALED返回真时,才应该使用。
WCOREDUMP(status) //返回真如果子进程导致内核转存。只有WIFSIGNALED返回真时,才应该使用。并非所有平台都支持这个宏,使用时应放在#ifdef WCOREDUMP ... #endif内部。
WIFSTOPPED(status) //返回真如果信号导致子进程停止执行。
WSTOPSIG(status) //返回导致子进程停止执行的信号数量。只有WIFSTOPPED返回真时,才应该使用。
WIFCONTINUED(status) //返回真如果信号导致子进程继续执行。




非 正常结束是什么呢???~~我们可以自己定义,比如tle,mle,runtime error,都会在子进程中发出相应的信号。如果接受到这些信号,则作出相应的动作。系统资源方面的限制可以通过getrlimit和setrlimit 来读取和设置。这两个函数都利用一个通用结构rlimit来描述资源限制。该结构定义在头文件sys/resource.h中,有两个成员rlim_t rlim_cur,rlim_t rlim_max,前者是软限制,后者是硬限制,我们可以通过设置软限制的值来控制子进程的流程。

有许多系统资源可以进行限制,它们由rlimit函数中的resource参数指定,并在头文件sys/resource.h中定义

resource参数
说    明

RLIMIT_CORE
内核转储(core dump)文件的大小限制(以字节为单位)
RLIMIT_CPU
CPU时间限制(以秒为单位)
RLIMIT_DATA
数据段限制(以字节为单位)
RLIMIT_FSIZE
文件大小限制(以字节为单位)
RLIMIT_NOFILE
可以打开的文件数限制
RLIMIT_STACK
栈大小限制(以字节为单位)
RLIMIT_AS
地址空间(栈和数据)限制(以字节为单位)

例如:
        getrlimit(RLIMIT_CPU, &limit);
        limit.rlim_cur = 1;
        setrlimit(RLIMIT_CPU, &limit);
     

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <signal.h>
#include <sys/wait.h>

int main()
{
   int status;
   struct rlimit memlim,timlim;
    getrlimit(RLIMIT_CPU,&timlim);
    getrlimit(RLIMIT_AS ,&memlim);
    memlim.rlim_cur=3*1024*1024; // MB
    timlim.rlim_cur=1;// s
 
    int pid = vfork();//用vfork可以保证子进程比父进程先运行
    if(pid==0)
    {
        setrlimit( RLIMIT_CPU,&timlim);
        setrlimit( RLIMIT_AS,&memlim);
        int *p = (int *)calloc(8*1024*1024, sizeof(int));
        if(-1==execl("/home/daniel/my_linux/apue/my_programming/my",\
						"my", (char *)0))
        {
            perror( "execl error\n ");
            exit(1);
        }
        exit(0);
    }
  wait(&status);
  printf("%d\n",status);
  if(!WIFEXITED(status))
  {
    int sig=0;
       //处理程序非正常结束状态
    if(WIFSIGNALED(status))
      sig=WTERMSIG(status);
    else
     return 1;
   // printf("%d\n",sig);
    if(sig == SIGXCPU)//24
        {
                printf("tle\n");
            }
            if(sig == SIGXFSZ)
        {
                printf("ole\n");
            }
            if(sig == SIGSEGV)
       {
                printf("re\n");
            }
            if(sig==SIGKILL) // 6
       {
                printf("mle\n");         
            }
           //if (WCOREDUMP(status))
        //printf("mle\n");

    }
  return 0;

}

其中my为:

#include <stdio.h>

int main()
{
	char *p = (char *)calloc(12*1024*1024, sizeof(char));
	while(1);
	return 0;
}



以上代码可以捕获到子进程超时(152, tle), 但是内存超限制后不能检测出问题,不知为什么?

setrlimit限制内存,没有起作用,为等等进一步实验~~



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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值