进程控制

一、进程的创建、终止和等待。

1.进程创建

fork():进程是处于执行期的程序以及相关资源的总称,进程在创建它的时候开始存活,在linux系统中,通过使用fork()函数复制一个现有进程来创建一个新进程,调用fork()函数的进程称为父进程,新产生的进程称为子进程,子进程几乎完整的复制了父进程,但至少pid不一样。调用fork()函数系统从内核返回两次:一次返回到父进程,另一次返回到新的子进程。

fork()函数的头文件:

#include<unistd.h>

fork()函数的函数原型:

pid_t  fork(void);子进程返回0,父进程返回子进程的pid,fork()函数一次创建两次返回。

exec():创建新的进程之后会立即执行新的进程,接着调用exec()这组函数就可以创建新的地址空间,并把新的地址空间载入其中。

fork()函数的实现

  • fork()函数采用了写时拷贝(copy-on-write)的技术,刚创建子进程的时候父进程和子进程拥有相同的地址空间(这些区域被设定为只读),只有在子进程或者父进程要写入数据的时候,父进程相关的地址空间才会被拷贝,父子进程指向相同的物理内存。
  • fork()之后是父进程先执行还是子进程先执行时不确定的,通过fork()函数创建的父子进程是共享一个文件表项的,linux通过clone()系统调用实现fork()函数。
  • 另外一个创建子进程的方法是调用vfork()函数,vfork()和fork()函数的区别是vfork()函数创建的子进程不拷贝父进程的页表,子进程在父进程的地址空间先运行,直到子进程退出后,父进程才可以继续运行。但是使用vfork()函数的时候,如果调用这两个函数之前子进程依赖于父进程的进一步动作,就会导致死锁。

fork()调用失败的原因:

  • 系统中有太多进程
  • 实际用户的进程数超过了限制

fork()与vfork()的区别

  1. fork():子进程拷贝父进程的数据段,代码段
    vfork():子进程和父进程共享数据段

  2. fork ()父子进程的执行次序不确定
    vfork ()保证子进程先运行,在调用exec 或exit 之前与父进程数据是共享的,在它调用exec或exit 之后父进程才可能被调度运行。

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

代码实现:
先看一个子进程拷贝父进程的代码段的例子:
这里写图片描述
运行结果:
这里写图片描述
fork的返回值有两个,子进程返回0,父进程返回子进程的进程号,进程号都是大于0的正整数,所以父进程的返回值一定大于0,在pid = fork();语句前只有父进程在运行,在pid=fork();之后父进程和新创建的子进程都在运行。而我们知道fork()函数子进程是拷贝父进程的代码段的,所以子进程中同样执行fork();以下的代码,这段代码会被父进程和子进程各执行一次。
再看一个拷贝数据段的例子:
这里写图片描述
运行结果:
这里写图片描述
那么为什么count不是2呢
因为fork()函数子进程拷贝父进程的数据段代码段,所以count++;将被父子进程各自执行一次,子进程执行时是使自己的数据段里的count+1;父进程执行时是使自己的数据段里面的count+1,它们互相独立,互相不影响。
那么再看一个vfork()的例子:
vfork()是共享数据段的,所以count应该是2(前提是子进程先执行,执行完后要调用exec或者exit之后父进程才能被调度运行)
这里写图片描述
运行结果:
这里写图片描述
注意:在子进程调度完成时。后面必须加上exit();或者exec();父进程才能执行,负责会产生死锁。子程序调用exit();之前与父进程数据是共享的,所以子程序退出后把父进程的数据段count改成了1,子进程退出后父进程继续执行,最终将count变为2。

2.进程终止

进程退出场景:

  • 代码运行完毕,结果正确。
  • 代码运行完毕,结果不正确。
  • 代码异常终止。

进程的终止方式
正常退出:

  1. 在main函数中执行return。
  2. 调用exit函数,并不处理文件描述符,多进程。
  3. 调用_exit或者 _Exit。

异常退出:
1. 调用abort,产生SIGABRT信号。
2. 由信号终止ctrl+c/SIGINT

exit和_exit的区别
这里写图片描述

1.清空缓冲区的操作。

int main(void)
{
    printf("hello world");
    //exit(0);//不需要程序员手工刷新缓冲区
    fflush(stdout);//不加这句话-不会刷新缓冲区--不会打印
    _exit(0);
}

2.exit会调用终止处理程序。
关于终止处理程序:

  • atexit可以注册终止处理程序,ANSI C规定最多可以注册32个终止处理程序。
  • 终止处理程序的调用与注册次序相反。
  • 函数原型:
    `int atexit(void(*function)(void));
    总结exit与_exit的区别
    1)_exit是一个系统调用,exit是一个c库函数。
    2)exit会执行刷新I/O缓存。
    3)exit会执行调用终止处理程序。

return和exit的区别:

  1. exit用于结束正在运行的整个程序,它将参数返回给操作系统,把控制权交给操作系统;而return是退出当前函数,返回函数值,把控制权交给调用函数。
  2. exit是系统调用级别,它表示一个进程的结束;而return是语言级别的,它表示调用堆栈的返回。
  3. 在main函数结束时,会隐式地调用exit函数,所以一般程序执行到main()结尾时,则结束主进程。exit将删除进程使用的内存空间,同时把错误信息返回给父进程,exit函数运行时会先执行由atexit()函数登记的函数,然后会做一些自身的清理工作,同时刷新所以输出流,关闭所有打开的流并且关闭通过标准I/O函数tempfile()创建的临时文件。
  4. void exit(int status);一般status为0,表示正常退出,非0表示异常退出。

3.进程等待

1).进程为什么等待?

首先,子进程退出,父进程如果不管不顾,就可能造成“僵尸进程”的问题,进而造成内存泄露。其次,我们需要知道父进程派给子进程的任务完成的如何。最后,父进程通过进程等待的方式,回收子进程资源,获得子进程退出信息。

2).进程怎么等待?

进行的等待就需要看到linux中两个接口

1.pid_t wait(int* status);
2.pid_t waitpid(pid_t pid,int* status,int options);

wait函数原型:

#include<sys/wait.h>
pid_t wait(int* status);

说明:wait函数会暂时停止目前进程的执行,直到有信号来到或者子进程结束。如果在调用wait时,子进程已经结束,则wait会立即返回子进程的结束状态值。如果不在意子进的返回状态,可将参数status设置为NULL。若成功返回子进程识别码(pid),若有错误发生则返回-1。

waitpid函数原型:

pid_t waitpid(pid_t pid,int* status,int options);

说明:
返回值:若成功,返回进程id,若出错,返回-1.waitpid会暂停目前的进程执行,直到有信号或者子进程结束。如果不在意结束时状态值,则status可以设置为NULL。参数pid为欲等待的子进程识别码。

pid<-1等待进程组识别码为pid绝对值的任何子进程;pid=-1等待任何子进程,此时就相当于wait();pid=0等待进程组识别码与目前进程相同的任何子进程;pid>0等待任何子进
程识别码为pid的子进程。

参数options可以设置为0或者与下面的组合:
(1)WNOHANG若没有任何已经结束的子进程则马上返回,不等待。
(2)WUNTRACED如果子进程进入暂停执行情况马上返回,但结束的状态不予理会。

代码实现

使用wait进行等待(阻塞式):
这里写图片描述
结果:
这里写图片描述
使用waitpid进行等待(非阻塞式):
这里写图片描述
运行结果:
这里写图片描述
关于WIFEXITED和WEXITSTATUS這两个宏:
(注意:這两个宏对于wait和waitpid都是可用的)
可以用這两个宏来检查wait和waitpid所返回的子进程的退出状态:
(1)如果WIFEXITED(status) 非0,则说明进程是正常退出的,不是异常退出的,那么我们可以使用WEXITSTATUS(status)来查看或者提取进程退出的返回值,例如:子进程要是调用exit(3),那么WEXITSTATUS(status)返回值就是3。

注意:如果如果WIFEXITED(status)为0,wait或者waitpid等待的那个子进程是异常退出,收到了信号而终止了运行。也只能说明這一点,這个时候的WEXITSTATUS(status),是毫无意义的,它只能返回正常退出的子进程的退出码,這一点要清楚。
3.wait和waitpid的区别:
(1).在任何一个子进程终止前,wait使其调用者阻塞,而waitpid有一个选项,可以使调用者不阻塞。
(2).waitpid并不等待在其调用之后的第一个终止的子进程。它有若干个选项,可以控制它所等待的进程。如果一个子进程已经终止,并且是僵尸进程,wait函数立即返回并取得该子进程的状态,否则wait使其调用者阻塞直到一个子进程终止。如果调用者阻塞并有多个子进程时,则在一个子进程终止时,wait立即返回,因为wait返回终止子进程的ID.

二、popen/system/fork/exec函数的区别

1.fork()
一个程序一但调用fork函数,系统就为一个新的进程准备了以下三个阶段。
(1)首先,系统让新的进程与旧的进程使用同一个代码段,因为它们的程序还是相同的,对于数据段和堆栈段,系统则复制一份给新进程,这样,父进程的所以数据都可以留给子进程,但是,子进程一旦开始运行,虽然它继承了父进程的一切数据,但实际上数据却已经分开,相互之间不再有影响了,也就是说,它们之间不再共享任何数据了。已经是两个进程了,对于父进程,fork函数返回了子程序的进程号,而对于子程序,fork函数则返回0.
(2)一般的,cpu都是以“页”为单位分配空间的,像INTEL的cpu,其一页在通常情况下是4k字节大小,而无论是数据段还是堆栈段都是由许多“页”构成的,fork函数复制这两个段,只是“逻辑”上的,并非是“物理”上的。
(3)实际执行fork时,物理空间上两个进程的数据段和堆栈段都还是共享的,当有一个进程写了某个数据时,这时两个进程之间的数据才有了区别,系统就将有区别的“页”从物理上也分开。系统在空间上的开销可以达到最小。

2.exec系列函数
一个进程一旦调用exec类函数,它本身就“死亡”了,系统把代码段替换成新的程序代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,唯一留下的就是进程号。

3.popen函数
它会通过command参数重新启动shell命令,并建立进程间的管道通信。

4.system函数
它会重新启动shell命令,当执行完毕后,程序会继续system下一行代码执行。

三、调研vfork创建的子进程, 直接return为什么会出现崩溃?

从前面我们知道,结束子进程的调用时exit()而不是return,如果你在vfork中return了,那么这就意味着main()函数return了,注意因为函数栈父子进程共享,所以整个程序的栈就出现问题了。如果你在子进程中return,那么基本就是下面的过程:
(1)首先子进程的main()函数return了。
(2)而mian()函数return后,通常会调用exit()或相似函数(如:exitgroup())。
(3)这时,父进程收到子进程exit(),开始从vfork返回,但是父进程的栈已经被子进程销毁了,因此程序就会直接崩溃。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值