创建进程、进程终止、进程等待及进程一些概念的补充

创建进程——fork函数

在linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

1.fork函数

头文件

#include <unisted.h>

函数

pid_t fork(void)

返回值

fork函数调用成功,返回两次

返回值为0                                   代表当前进程是子进程

返回值为非负数(id)                代表当前进程是父进程

调用失败,返回-1

fork返回的值又叫pid,全称叫进程标识符,每个进程都有一个非负整数表示的唯一ID,叫做pid

pid=0:交换进程(swapper)        作用:进程调度

pid=1:init进程                                作用:系统初始化

示例代码如下

int main(void)
{
	pid_t pid;
	printf("Before: pid is %d\n", getpid());
	if ((pid = fork()) == -1)perror("fork()"), exit(1);
	printf("After:pid is %d, fork return %d\n", getpid(), pid);
	sleep(1);
	return 0;
}
 
运行结果:
[root@localhost linux]# . / a.out
Before : pid is 43676
After : pid is 43676, fork return 43677
After : pid is 43677, fork return 0

我们可以从上面的代码发现,在使用fork之后,pid的值出现了两种分别是43676、43677,但是其中有一项与没使用fork时,一样,用下面一张图来对其进行解释。

我们可以看到上述代码中并没有对父子进程进行条件限制,那么在程序运行起来时,在 fork之后,会先执行父进程还是子进程呢?实际上,fork之后,谁先执行完全由调度器决定。

(1)如何理解 fork返回后,给父进程返回子进程的 pid,给子进程返回 0?

因为一个父亲的孩子可以有很多个,可是每个孩子都只有一个父亲。也就是说,孩子找父亲是具有唯一性的。以此类推,子进程 fork之后,不需要父进程的 id值,因为父进程具有唯一性。而父进程 fork之后需要对应子进程的 id,因为该父进程可能不止一个子进程,它需要对应的子进程 id做标识。

(2)如何理解同一个 id值,会返回两个不同的值,让 if 和 else if 同时执行

返回的本质:就是写入。我们不知道父子进程谁先返回,谁先返回,谁就先写入 id值。由于进程具有独立性,进程在执行 fork相应代码时,会在操作系统内部进行写时拷贝,使 fork对应的进程可以返回两个不同的值,再让对应的父子进程根据自己返回的 id,去执行 if 或 else if 中的代码内容。
 

(3)理解写时拷贝

在理解写时拷贝之前,看一下下面这个示例代码:

观察代码中的a_val的值和地址。

用子进程改变a_val的值,但是父进程中的a_val未改变,且地址一样。如果它的地址是指向内存的,那么下面的情况很矛盾。

  

通常情况下,父子代码共享,父子进程在不写入(不修改共享部分的数据)时,对应的数据也是共享的。当任意一方试图写入,操作系统便会以写时拷贝的方式,给需要修改的一方在物理内存中开辟一块新空间,将原来的数据拷贝到新空间中,再对新空间中的数据做修改。具体见下图:

 

2.fork常规用法

一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

3.fork调用失败的原因

系统中有太多的进程
实际用户的进程数超过了限制(一个用户可以创建的进程是有限制的)

4.vfork函数

#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
功能:
	vfork() 函数和 fork() 函数一样都是在已有的进程中创建一个新的进程,但它们创建的子进程是有区别的。

返回值:
    成功:子进程中返回 0,父进程中返回子进程 ID。pid_t,为无符号整型。
    失败:返回 -1。

(1)fork() 与 vfock() 的区别

fork(): 父子进程的执行次序不确定。
vfork():保证子进程先运行,在它调用 exec(进程替换) 或 exit(退出进程)之后父进程才可能被调度运行。
fork(): 子进程拷贝父进程的地址空间,子进程是父进程的一个复制品。
vfork():子进程共享父进程的地址空间(准确来说,在调用 exec(进程替换) 或 exit(退出进程) 之前与父进程数据是共享的)

代码示例:

#include<stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include<stdlib.h>
int main()
{
        pid_t pid;
        pid_t fpid;
        pid=getpid();
        int count=0;
        fpid=vfork();
        if(fpid>0){
                while(1){
                        printf("这是父进程,PID=%d,count=%d\n",pid,count);
                        sleep(1);
                }
        }
        else if(fpid==0){
                while(1){
                        printf("这是子进程,PID=%d\n",getpid());
                        sleep(1);
                        count++;
                        if(count==3){
                                exit(0);
                        }
                }
        }
        return 0;
}
以下是程序运行的结果:
这是子进程,PID=17935
这是子进程,PID=17935
这是子进程,PID=17935
这是父进程,PID=17934,count=3
这是父进程,PID=17934,count=3
这是父进程,PID=17934,count=3
由此可看出由vfork创建的子进程在退出前共享父进程地址空间
因为在子进程退出时父进程没有收集子进程的状态,所以子进程变为僵尸进程。z+表示僵尸进程,s+表示正在运行。
fhn       17999  0.0  0.0      0     0 pts/2    Z+   21:03   0:00 [vfork] <defunct>

退出进程——exit函数

1.进程退出的方式:

正常退出:

  • main函数调用return
  • 进程调用exit()函数(标准C库)
  • 进程调用_exit()或_Exit()
  • 进程最后一个线程返回
  • 最后一个线程调用pthread_exit()

异常退出:

  • 调用abort
  • 当线程收到某些信号时,如:ctrl+c
  • 最后一个线程对取消(cancellation)请求做出响应 

2.退出方式比较:

  • exit和return的区别:exit是一个函数,有参数;而return是函数执行完后的返回。exit把控制权交给系统,而return将控制权交给调用函数。
  • exit和abort的区别:exit是正常终止进程,而about是异常终止。
  • exit(int exit_cod):exit中的参数exit_code为0代表进程正常终止,若为其他值表示程序执行过程中有错误发生,比如溢出,除数为0。
  • exit()和_exit()的区别:exit头文件stdlib.h中声明,而_exit()声明在头文件unistd.h中。两个函数均能正常终止进程,但是_exit()会执行后立即返回给内核,而exit()要先执行一些清除操作,然后将控制权交给内核。

3.父子进程终止的先后顺序不同会产生不同的结果。

  • 在子进程退出前父进程退出,则系统会让init进程接管子进程。
  • 当子进程先于父进程终止,而父进程又没有调用wait函数等待子进程结束,子进程进入僵死状态,并且会一直保持下去除非系统重启。(子进程处于僵死状态是,内核只保存该进程的一些必要信息以备父进程所需。此时子进程始终占用着资源,同时也减少了系统可以创建的最大进程数。)
  • 子进程先于父进程终止,且父进程调用了wait或waitpid函数,则父进程会等待子进程结束。

 4.函数原型

(1)exit()函数

#include <unistd.h>
void exit(int status);

参数:status 定义了进程的终止状态

exit最后也会调用 _exit, 但在调用 _exit之前,还做了其他工作:

  1. 执行用户通过 atexit或on_exit定义的清理函数;
  2. 关闭所有打开的流,所有的缓存数据均被写入;
  3. 调用_exit。

 (2)_exit()函数

#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值

5.补充

  1. 进程退出会变成僵尸,同时该进程也会把自己对应的退出码写入到自己的 task_struct中
  2. wait/waitpid 是一个系统调用,也就是说,它们是由 OS完成的,OS有能力去读取子进程的task_struct;
  3. 所以,父进程获取到的子进程退出信息,是从退出子进程的 task_struct中获取到的。

 

等待进程——wait函数

在讲解等待进程之前,我们先聊一聊僵尸进程及进程等待的重要性。

1.僵尸进程

当一个进程变为僵尸状态的时候,该进程就变成了僵尸进程。

  • 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
  • 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
  • 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。

(1)僵尸进程的危害

 

(2)通过代码来模拟僵尸状态的进程

#include<stdio.h>
#include<stdlib.h>                                                            
#include<unistd.h>
  int main()
   {
     	pid_t id=fork();
   		int count=5;
   		while(1)
  		{
     		if(id==0)
    		{
       			while(1){
     			printf("i am process..child---.pid:%d,ppid:%d\n",getpid(),getppid());
    			sleep(1);
   				}
     		 	printf("child quit....\n");
    		 	exit(1);
   			}
    		else if(id>0)
     		{
     			while(count)
     			{
     				printf("i am process..father---pid:%d,ppid:%d\n",getpid(),getppid());
     				count--;
     				sleep(1);
     			}
     			exit(0);
   			}
  		}
 		return 0;
	 }

while :; do ps axj |head -1&&ps axj|grep a.out;echo "#######################";sleep 1;done来监控进程的状态。

(3)进程等待的必要性

  • 子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
  • 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
  • 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。

 2.wait()函数

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
参数status用来保存被收集进程退出时的一些状态
它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉的毫不在意
只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL。

可使用wait函数传出参数status来保存进程的退出状态。借助宏函数来进一步判断进程终止的具体原因。宏函数可分为如下三组:
1.  WIFEXITED(status) 为非0 → 进程正常结束

	WEXITSTATUS(status) 如上宏为真,使用此宏 → 获取进程退出状态 (exit的参数)

 2. WIFSIGNALED(status) 为非0 → 进程异常终止

	WTERMSIG(status) 如上宏为真,使用此宏 → 取得使进程终止的那个信号的编号。

3. WIFSTOPPED(status) 为非0 → 进程处于暂停状态

	WSTOPSIG(status) 如上宏为真,使用此宏 → 取得使进程暂停的那个信号的编号。

	WIFCONTINUED(status) 为真 → 进程暂停后已经继续运行
	
//下面是使用方法:注意&status是指针
wpid = wait(&status)
if(WIFEXITED(status)){	//正常退出
			printf("I'm parent, The child process "
					"%d exit normally\n", wpid);
			printf("return value:%d\n", WEXITSTATUS(status));
 
		} 

返回值:
如果成功,wait会返回被收集的子进程的进程ID
如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。

3.waitpid()函数

pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
    当正常返回的时候,waitpid返回收集到的子进程的进程ID;
    如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
    如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
     pid:
         pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定        
         的子进程还没有结束,waitpid就会一直等下去。
         pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。
         pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对 
         它做任何理睬。
         pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。

     status:
         WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
         WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
     options:
         WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。

4.获取子进程status

  • wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
  • 如果传递NULL,表示不关心子进程的退出状态信息。
  • 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
  • status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)
 代码示例1(代码正常结束、运行正确)
#include<sys/types.h>
#include<sys/wait.h>
#include<stdio.h>
 
int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        //子进程
        int cnt = 5;
        while (cnt)
        {
            printf("我是子进程: %d, 父进程: %d, cnt: %d\n", getpid(), getppid(), cnt--);
            sleep(1);
        }
 
        exit(10); //进程退出
    }
 
    // 父进程
    int status = 0; // 不是被整体使用的,有自己的位图结构
    pid_t ret = waitpid(id, &status, 0);
    if (id > 0)
    {
        printf("wait success: %d, sig number: %d, child exit code: %d\n", ret, (status & 0x7F), (status >> 8) & 0xFF);
    }
 
    sleep(5);
}

信号编号(终止信号)为低七位,我们可以通过 status & 0x7F获得;

退出码(在退出状态中)在次低八位,我们可以通过 (status>>8)&0xFF获得;

在程序正常运行的情况下,使用 exit返回,终止信号为0,退出码为 exit(?) 的 ?值。

代码示例2(野指针)
#include<sys/types.h>
#include<sys/wait.h>
#include<stdio.h>
 
int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        //子进程
        int cnt = 5;
        while (cnt)
        {
            printf("我是子进程: %d, 父进程: %d, cnt: %d\n", getpid(), getppid(), cnt--);
            sleep(1);
            int* p = NULL;//野指针
            *p = 100;
        }
 
        exit(10); //进程退出
    }
 
    // 父进程
    int status = 0; // 不是被整体使用的,有自己的位图结构
    pid_t ret = waitpid(id, &status, 0);
    if (id > 0)
    {
        printf("wait success: %d, sig number: %d, child exit code: %d\n", ret, (status & 0x7F), (status >> 8) & 0xFF);
    }
 
    sleep(5);
}

在程序非正常运行时,使用 exit返回,终止信号为对应的信号值,退出码为0。终止信号不为0,代表非正常退出。

代码示例3(借助status值,检验子进程是否正常退出)
int main()
{
    pid_t id = fork();
    assert(id != -1);
    if (id == 0)
    {
        //child
        int cnt = 10;
        while (cnt)
        {
            printf("child running, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
            sleep(1);
            //    int *p = 0;
            //    *p = 100; //野指针问题
        }
 
        exit(10);
    }
 
    int status = 0;
    // 1. 让OS释放子进程的僵尸状态
    // 2. 获取子进程的退出结果
    // 在等待期间,子进程没有退出的时候,父进程只能阻塞等待
    int ret = waitpid(id, &status, 0);
    if (ret > 0)
    {
        // 是否正常退出
        if (WIFEXITED(status))
        {
            // 判断子进程运行结果是否ok
            printf("exit code: %d\n", WEXITSTATUS(status));
        }
        else {
            //TODO
            printf("child exit not normal!\n");
        }
        //printf("wait success, exit code: %d, sig: %d\n", (status>>8)&0xFF, status & 0x7F);
    }
 
    return 0;
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值