fork函数
来看点干货吧
- fork函数是创建进程的方法,它的特征是一次调用,两次返回。fork函数被父进程调用一次,但是却返回两次——一次是返回到父进程,一次是返回到新创建的子进程。
- 新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程人和打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程与子进程最大的区别在于他们有不同的pid。
- 在父进程中,fork返回子进程的PID,在子进程中,fork返回0,如果出错则返回-1。
我们来看看老师提供给我们的例子吧
- 看例子前先看一下这些代码的头文件吧
#include <stdlib.h>//里面有atexit函数
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
第一个
void fork0()
{
if (fork() == 0) {
printf("Hello from child\n");
}
else {
printf("Hello from parent\n");
}
}
首先我们把它的进程图画出来
它在Linux环境下的运行结果为
那我们接下看第二个例子
void fork1()
{
int x = 1;
pid_t pid = fork();
if (pid == 0) {
printf("Child has x = %d\n", ++x);
}
else {
printf("Parent has x = %d\n", --x);
}
printf("Bye from process %d with x = %d\n", getpid(), x);
}
我们看一下它的进程图
在Linux环境下的运行结果为
我们来看第三个例子
void fork2()
{
printf("L0\n");
fork();
printf("L1\n");
fork();
printf("Bye\n");
}
它的进程图为
我们在看一下它在Linux环境下的运行结果
与之前的情况一样先执行完父进程,再执行子进程,一共是输出7个。
来看第四个例子
void fork3()
{
printf("L0\n");
fork();
printf("L1\n");
fork();
printf("L2\n");
fork();
printf("Bye\n");
}
这个例子其实与第三个例子差不多,只是多了一个fork
来看看它的进程图
它在Linux环境下的运行结果为
第五个例子
void fork4()
{
printf("L0\n");
if (fork() != 0) {
printf("L1\n");
if (fork() != 0) {
printf("L2\n");
}
}
printf("Bye\n");
}
它的进程图
在Linux环境下的运行结果为
第六个例子
void fork5()
{
printf("L0\n");
if (fork() == 0) {
printf("L1\n");
if (fork() == 0) {
printf("L2\n");
}
}
printf("Bye\n");
}
该例子与第五个例子类似
我们看看它在Linux环境下的运行结果
第七个例子
void cleanup(void) {
printf("Cleaning up\n");
}
void fork6()
{
atexit(cleanup);
fork();
exit(0);
}
- 我们看到该函数中多了一个atexit()函数,那它是用来干什么的呢?这个函数是什么意思呢?
- atexit()函数是用来注册程序正常终止(也就是通过exit(0)、_exit(0)或return结束的程序)时要被调用的函数。很多时候我们需要在程序退出时做一些诸如释放的操作,但程序退出的方式有很多种,比如main()函数运行结束、在程序的某个地方用exit()结束程序、用户通过Ctrl+C操作来终止程序等等,因此需要有一种与程序退出方式无关的方法来进行程序退出时的必要处理。
详细解释请看
———————————————
版权声明:本文为CSDN博主「tf_apologize」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/tf_apologize/article/details/53162218
它的运行结果为
第八个例子
void fork7()
{
if (fork() == 0) {
/* Child */
printf("Terminating Child, PID = %d\n", getpid());
exit(0);
}
else {
printf("Running Parent, PID = %d\n", getpid());
while (1)
; /* Infinite loop */
}
}
我们来看看它的进程图
- 由于子进程在printf后有exit函数,所以子进程会终止,但是父进程在printf后却有一个while(1)的循环,所以在子进程终止后父进程是无法结束的,导致shell无法出来。此时只能进行强制性的终止。
Ctrl+Z为挂起,Ctrl+C为终止。 - 我们来看一下它的结果
- 接下来我们使用ps命令
- 不过我不知道ps命令是用来干什么的,所以我们就来看看它有什么用吧
- ps命令是最基本的进程查看命令。使用该命令可以确定有哪些进程正在运行和运行的状态、进程是否结束、进程有没有僵尸、哪些进程占用了过多的资源等等。ps显示瞬间进程的状态,并不动态连续;如果想对进程实行监控应该用top命令。
- 在Linux中进程有五种状态运行状态及状态码
- 运行状态(正在运行或在运行队列中等待[就绪队列]) R
- 中断状态(休眠中, 受阻, 在等待某个条件的形成或接受到信号) S
- 不可中断状态(收到信号不唤醒和不可运行, 进程必须等待直到有中断发生) D
- 僵死状态(进程已终止, 但进程描述符存在, 直到父进程调用wait4()系统调用后释放) Z
- 停止状态(进程收到SIGSTOP, SIGSTP, SIGTIN, SIGTOU信号后停止运行运行) T
- PID为进程的id号
- TTY为登入者的终端机位置
- TIME为使用掉的CPU时间
- CMD为所下达的指令名称
- 相关参数:
- -e显示所有进程
- -f全格式
- -h不显示标题
- -l长格式
- -w 宽输出
- a 显示终端上的所有进程,包括其他用户的进程
- r 只显示正在运行的进程
- x 显示没有控制终端的进程
- ps -ef 查看全格式的全部进程
- ps -ax 查看全部进程
- ps -ef|grep <进程名> 查看并筛选 跟进程名有关的进程,该进程名可以是进程的全部或者部分。
- 详细知识可见https://baike.baidu.com/item/PS/8850709?fr=aladdin
- 那我们来看看使用ps 命令后的结果为什么吧
我们可以看到在父进程的PID后面的fork后有一个,defunct的中文翻译为僵尸进程 - 僵尸进程(defunct)的形成:在Linux系统中,一个进程结束了,但是他的父进程没有等待(调用wait/waitpid)他,那么它将变成一个僵尸进程。当用ps命令观察进程的执行状态时,看到这些进程的状态栏为defunct。僵尸进程是一个早已死亡的进程,但在进程表中仍然占了一个位置。如果该进程的父进程已经先结束了,那么该进程就不会变成僵尸进程。
- 那僵尸进程会占用位置,有什么办法可以杀死它吗?
这时我们可以使用命令 kill -9 加上父进程的ID
我们来看看是否真的可以杀死吧
(该张图片的PID与之前的不同,是因为我又把这个程序运行了一遍,所以他的PID与之前不同了) - 在写这个的时候我以为kill就是杀死进程,不知道为什么要加上-9,这个周老师给我们讲了信号,也就知道了这个的含义。
- 函数kill是发送信号的函数,它并不能直接杀死进程。
- 函数kill位于头文件
#include <sys/types.h>
与#include <signal.h>
中 - kill函数的形式是
int kill(pid_t pid,int sig);
- 函数的第一个参数pid,如果pid大于零,那么kill函数发送信号号码sig给进程pid;如果pid等于零,那么kill发送sig给调用进程所在进程组中的每个进程,包括调用进程自己;如果pid小于零,那么kill发送信号sig给进程组中|pid|(pid绝对值)中的每个进程。
详细的有关僵尸进程defunct的知识可见https://blog.csdn.net/wangjingyu00711/article/details/41958205
我们来看第九个例子吧
void fork8()
{
if (fork() == 0) {
/* Child */
printf("Running Child, PID = %d\n",
getpid());
while (1)
; /* Infinite loop */
}
else {
printf("Terminating Parent, PID = %d\n",
getpid());
exit(0);
}
}
我们来看看它的进程图吧
那它在Linux环境中的执行结果为什么呢?
在这个程序中,父进程是先执行完的,根据上面的知识是不会产生僵尸进程的,但是我在csdn上看了其他同学的博客,有一个同学的运行结果这个程序是也会产生僵尸进程的,而且他还使用了kill来杀死程序。我也不知道我的对不对,但是在我的系统中它是没有僵尸进程的。
我们来看第十个例子
void fork9()
{
int child_status;
if (fork() == 0) {
//child
printf("HC: hello from child\n");
exit(0);
}
else {
printf("HP: hello from parent\n");
wait(&child_status);
printf("CT: child has terminated\n");
//parent
}
printf("Bye\n");
}
我还不太知道wait函数是一个怎样的函数,所以我们先来看看程序在linux环境下的运行结果吧
从运行结果可以知道父进程在执行完第一条printf后遇到wait函数,转接着去执行子进程,子进程执行完后又去执行父进程。
我们来看看wait到底是个什么函数吧
- wait:进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁掉后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
- 参数status用来保存被收集进程退出时的一种状态,他是一个指向int类型的指针。但如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数的情况下,我们都会这样去想的),我们就可以把这个参数设为NULL,就像这样
pid = wait(NULL)
如果成功,wait会返回被收集的子进程ID,如果调用进程没有子进程,调用就会失败,此时wait就会返回-1,同时errno就会置为ECHILD。
我们知道了这些后来看看它的进程图吧
第十一个例子
#define N 5
void fork10()
{
pid_t pid[N];
int i, child_status;
for (i = 0; i < N; i++)
if ((pid[i] = fork()) == 0) {
exit(100+i); /* Child */
}
for (i = 0; i < N; i++) { /* Parent */
pid_t wpid = wait(&child_status);
if (WIFEXITED(child_status))
printf("Child %d terminated with exit status %d\n",wpid, WEXITSTATUS(child_status));
else{
printf("Child %d terminate abnormally\n", wpid);}
}
}
- 代码中的
WIFEXITED(child_status)
是返回一个正常终止的子进程的退出状态。只有在WIFEXITED()为真时,才会定义这个状态。 - 我们来看在Linux环境下的运行结果
第十二个例子
#define N 5
void fork11()
{
pid_t pid[N];
int i;
int child_status;
for (i = 0; i < N; i++)
if ((pid[i] = fork()) == 0)
exit(100+i); /* Child */
for (i = N-1; i >= 0; i--) {
pid_t wpid = waitpid(pid[i], &child_status, 0);
if (WIFEXITED(child_status))
printf("Child %d terminated with exit status %d\n",
wpid, WEXITSTATUS(child_status));
else
printf("Child %d terminate abnormally\n", wpid);
}
}
其实它与第十一个例子类似
它的运行结果为
第十三个例子
#define N 5
void fork12()
{
pid_t pid[N];
int i;
int child_status;
for (i = 0; i < N; i++)
if ((pid[i] = fork()) == 0) {
/* Child: Infinite Loop */
while(1)
;
}
for (i = 0; i < N; i++) {
printf("Killing process %d\n", pid[i]);
kill(pid[i], SIGINT);
}
for (i = 0; i < N; i++) {
pid_t wpid = wait(&child_status);
if (WIFEXITED(child_status))
printf("Child %d terminated with exit status %d\n",
wpid, WEXITSTATUS(child_status));
else
printf("Child %d terminated abnormally\n", wpid);
}
}
父进程在进行五次fork后子进程一直处于循环中,之后父进程杀死子进程,父进程等待子进程正常结束,结束进程。
第十四个例子
/*
* int_handler - SIGINT handler
*/
void int_handler(int sig)
{
printf("Process %d received signal %d\n", getpid(), sig); /* Unsafe */
exit(0);
}
/*
* fork13 - Simple signal handler example
*/
void fork13()
{
pid_t pid[N];
int i;
int child_status;
signal(SIGINT, int_handler);
for (i = 0; i < N; i++)
if ((pid[i] = fork()) == 0) {
/* Child: Infinite Loop */
while(1)
;
}
for (i = 0; i < N; i++) {
printf("Killing process %d\n", pid[i]);
kill(pid[i], SIGINT);
}
for (i = 0; i < N; i++) {
pid_t wpid = wait(&child_status);
if (WIFEXITED(child_status))
printf("Child %d terminated with exit status %d\n",
wpid, WEXITSTATUS(child_status));
else
printf("Child %d terminated abnormally\n", wpid);
}
}
它的运行结果为
呃呃呃呃呃呃又来了一个signal函数,那我们又来了解一下吧
- signal函数
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);
- 第一个参数是指信号名称,接收到了什么类型的信号;
- 第二个参数是接收到信号后转而要去执行的函数;
- 我们需要记住的信号有
- 2 SIGINT 来自键盘的中断(在Linux系统中相当于Ctrl+C,指令kill -2)
- 9 SIGKILL 杀死程序(kill -9)
- 14 SIGALRM 来自alarm函数的定时器信号
- 17 SIGCHLD 信号为忽略 一个子进程停止或者终止(发送给父进程)
第十五个例子
void fork14()
{
pid_t pid[N];
int i;
ccount = N;
signal(SIGCHLD, child_handler);
for (i = 0; i < N; i++) {
if ((pid[i] = fork()) == 0) {
sleep(1);
exit(0); /* Child: Exit */
}
}
while (ccount > 0)
;
}
/*
* child_handler2 - SIGCHLD handler that reaps all terminated children
*/
void child_handler2(int sig)
{
int child_status;
pid_t pid;
while ((pid = wait(&child_status)) > 0) {
ccount--;
printf("Received signal %d from process %d\n", sig, pid); /* Unsafe */
fflush(stdout); /* Unsafe */
}
}
这个是它在Linux环境下的运行结果
但是我有点看不太懂这个程序,它应该也是一个僵尸进程,因为shell命令行没有显示。
按下Ctrl+Z时它会挂起
ps命令后可以看到它是有四个僵尸进程,我输入kill -9也不会杀死进程,唉,我太蠢了
十六个例子
void child_handler(int sig)
{
int child_status;
pid_t pid = wait(&child_status);
ccount--;
printf("Received SIGCHLD signal %d for process %d\n", sig, pid); /* Unsafe */
fflush(stdout); /* Unsafe */
}
void fork15()
{
pid_t pid[N];
int i;
ccount = N;
signal(SIGCHLD, child_handler2);
for (i = 0; i < N; i++)
if ((pid[i] = fork()) == 0) {
sleep(1);
exit(0); /* Child: Exit */
}
while (ccount > 0) {
pause();
}
}
运行结果为
十七个例子
void fork16()
{
if (fork() == 0) {
printf("Child1: pid=%d pgrp=%d\n",
getpid(), getpgrp());
if (fork() == 0)
printf("Child2: pid=%d pgrp=%d\n",
getpid(), getpgrp());
while(1);
}
}
getpgrp()是用来取得目前进程所属的组识别码,相当于调用getpid()
可以看到子进程的组识别码是一样的
ps后的结果为
第十八个例子
void fork17()
{
if (fork() == 0) {
printf("Child: pid=%d pgrp=%d\n",
getpid(), getpgrp());
}
else {
printf("Parent: pid=%d pgrp=%d\n",
getpid(), getpgrp());
}
while(1);
}
对于进程组ID,所有的子进程识别码ID与父进程识别码ID相同。对于进程组ID一般来说,一个进程在shell下执行,shell程序就将该进程的进程组PID赋给该进程的进程组的ID,从该进程派生的子进程都拥有福进程所属的进程组ID,除非父进程将子进程的所属组ID设置成与该子进程的PID一样。