进程控制 | 认识fork函数 | 进程终止 | 进程等待

进程创建

初始fork函数

fork函数是为了创建子进程而生的,通过fork函数之后,我们的父进程的代码和数据是共享的,我们这里是可以通过man手册进行查询的,查询之后是可以发现fork函数是会返回两个值的至于为什么会返回两个值,我们下面来进行探讨。

首先在我们之前的文章里讲过,fork函数是会进行返回两个值的,这个值相当于给父进程进行写入的。而且我们如果通过打印的化不难发现父进程返回的值就是我们创建出来子进程的pid,而我们的子进程在fork函数之后返回的其实是0,我们之前解释过为什么fork函数之后是会返回两个值的。

解释原因:进程其实本质上就是内核的某种数据结构(地址空间(mm_struct)+ 页表 +PCB(Linux下是task_struct))+ 进程的代码和数据组成的,然后在我们父进程如果进行创建子进程的过程中,子进程的代码和数据其实就是继承父进程的代码和部分数据,这里我们需要注意的是子进程的代码和父进程的代码是共享的,但是我们子进程的数据和父进程的数据其实是独立的,这就造成我们的返回值就可能是两个,还有一个原因就是我们的数据大部分其实都是拷贝父进程的数据,但是有些数据会进行写实拷贝,啥是写时拷贝我后面再讲。

这样就确保我们每个进程都是独立的,也就导致最后我们的返回值是两个

所以fork函数之后我们的操作系统其实上做了四点

分配新的内存块和内核数据结构给子进程

将父进程部分数据结构内容拷贝至子进程
添加子进程到系统进程列表当中
fork 返回,开始调度器调度

 fork函数返回值的意义

子进程返回的就是0

父进程返回的就是子进程的pid

fork函数的常规用法

我们知道fork函数是为了创建子进程的,那就证明子进程肯定是为了给父进程分担一些任务的,比如父进程一写任务就会分给子进程,还有的时候子进程会会自己单独执行一个程序

fork函数失败

fork函数有的时候创建进程的时候也是会失败的,比如当我们的进程队列过多的时候,fork函数就是会失效,这和我们以前文章里不断进行malloc开空间是一样的道理,我们在堆上开空间的时候,本质就是给我们的程序创造空间,程序最后实际上就是一个一个的进程,所以fork函数也是会失败的。

所以fork 之后创建一批结构,代码以共享的方式,数据以写时拷贝的方式,两个进程必须保证 "独立性",做到互不影响。在这种共享机制下子进程或父进程任何一方挂掉,不会影响另一个进程。

写时拷贝

写时拷贝能让我们的进程具有独立性,其中最大的特点应该是发生在数据上的不同。

为了让大家更好的理解写时拷贝,我们先引出话题,我们为什么要进行写时拷贝。

解释:fork之后我们可以认为我们子进程是继承父进程的代码和数据,但是我们知道我们的进程是具有独立性的,看视两个相互矛盾的话题,这就引出了我们为什么要进行写时拷贝的原因了,我们如果为了确保独立性的化为什么不直接拷贝一份数据给子进程,父子进程的代码和数据都拷贝一份自己的不就行了

但是这就是会造成空间的浪费,我们子进程的大部分数据都是可以用父进程的,所以如果拷贝一份确实会浪费空间,我们这里也可以给出一个结论就是我们父子进程进行继承的时候,子进程是会拷贝一份父进程的“东西的”,他们分别就是task_struct + mm_struct(地址空间) + 页表 + 代码和数据。

所以就能很好的减少成本,提高我们操作系统的效率了。

总结

  • 有浪费空间之嫌:父进程的数据,子进程不一定全用;即便使用,也不一定全部写入。
  • 最理想的情况,只有会被父子修改的数据,进行分离拷贝。不需要修改的数据,共享即可。但是从技术角度实现复杂。
  • 如果 fork 的时候,单独生成一份子进程,会增加 fork 的成本(内存和时间)

所以这就是我们为什么要进行写时拷贝的原因,我们下面可以通过一些图片来了解写时拷贝到底是什么

了解写时拷贝的过程

我们写时拷贝的时候通过这个图就可以看到虚拟地址是不变的,但是通过页表进行映射的时候,在内存种指向的空间就会发生拷贝,我们可以认为新开出来的内存就是发生了写时拷贝的这样一个过程。

这里的写时拷贝的时候还是会发生一个小细节的,我们不要以为创建子进程之后马上就是会发生写时拷贝的,其实不是的,我们只会在我们要用这个的时候,才会进行使用,我们可以认为操作系统对我们这个过程是有延时的,因为我们可能不是马上就用这个空间,只是开了出来,这和我们C语言学的是一样的比如就是我们的malloc函数,我们使用的时候是不是先开出来空间,然后才是进行的使用,而不是开出来就马上进行使用,对吧!!!!

进程的终止

我们是可以给我们的进程分成三种情况的

第一种 :代码正常运行 代码跑完,结果也是正确的

第二种:代码运行,代码跑完之后,但是结果是不正确的

第三种:就是异常,如果在我们语言方面是可以讲成程序崩溃

下面有个问题,我们为什么要进行进程终止???进程终止究竟是在干嘛???

在这之前我们还要讲一个东西,我们都知道我们如果写一个代码的化,然后进行预处理,编译,汇编,链接这四步之后,我们会产生我们的可执行的程序,我们可执行程序如果要运行,就会形成进程,进程其实本质上就是内核的某种数据结构(地址空间(mm_struct)+ 页表 +PCB(Linux下是task_struct))+ 进程的代码和数据组成的,这我们上面是讲过的,但是这里我们还是需要想想我们是先将我们的代码和数据放在内存里,还是先创建task_struct和地址空间和页表呢??
我们可以这么想,在我们上大学的时候,我们填完志愿之后,学校录取你之后,是你马上就要去学校吗?我们肯定是会在要开学的时候才是会去学校的,这个时候我们的信息是不是已经在学校里了,但是我们人还是不需要去学校的,所以我们的进程就是先创建PCB和地址空间还有我们的页表。然后才是我们程序的代码和数据。

那话又说回来,进程终止到底是在干嘛呢,答案就是先释放我们的代码和数据,然后就是我们进程的内核的数据结构,释放的顺序大家一定要记住的,我们后面就会讲退出码和信号的时候,就会用到。

我们先来写一下世界上最简单的代码。

这么简单的代码我肯定是故意打错的。

我们来改一下我们的代码吧.

[tjl@hecs-67680 3_22]$ make
gcc -o myprocess myprocess.c
[tjl@hecs-67680 3_22]$ ./myprocess 
hello word
[tjl@hecs-67680 3_22]$ 

看完操作之后来看看我们的代码

#include <stdio.h>
int main()
{
  printf("hello word\n");
  return 0;
}

接下来我们需要思考的就是我们为什么每次代码都需要return 0 ,为什么不是return 100 这些,难道它这个返回值也是有不同的意义的。

进程的退出码

我们在我们主函数程序结束的时候会返回一个值,这个值就是我们的退出码。

退出码是很重要的,它主要就是给我们的父进程bash来看的,这也就有有了我们上面进程终止的三种情况。

第一种 :代码正常运行 代码跑完,结果也是正确的

第二种:代码运行,代码跑完之后,但是结果是不正确的

第三种:就是异常,如果在我们语言方面是可以讲成程序崩溃

echo $?

我们用这个指令就是可以查询我们的退出码了。

我们之前说过ls这些简单的指令也是一个进程,我们也可以查出他们的退出码。

[tjl@hecs-67680 3_22]$ ls adafa
ls: cannot access adafa: No such file or directory
[tjl@hecs-67680 3_22]$ echo $?
2
[tjl@hecs-67680 3_22]$ 

当我们输入一个不认识的指令,它也是会进行返回一个值的,所以这个值是有它自己的意思,我们可以来写一个代码,来查看这些退出码的信息,这个时候我们需要认识一个函数。

那我们来写一个循环的代码来进行查询。

0 :Success
1 :Operation not permitted
2 :No such file or directory
3 :No such process
4 :Interrupted system call
5 :Input/output error
6 :No such device or address
7 :Argument list too long
8 :Exec format error
9 :Bad file descriptor
10 :No child processes
11 :Resource temporarily unavailable
12 :Cannot allocate memory
13 :Permission denied
14 :Bad address
15 :Block device required
16 :Device or resource busy
17 :File exists
18 :Invalid cross-device link
19 :No such device
20 :Not a directory
21 :Is a directory
22 :Invalid argument
23 :Too many open files in system
24 :Too many open files
25 :Inappropriate ioctl for device
26 :Text file busy
27 :File too large
28 :No space left on device
29 :Illegal seek
30 :Read-only file system
31 :Too many links
32 :Broken pipe
33 :Numerical argument out of domain
34 :Numerical result out of range
35 :Resource deadlock avoided
36 :File name too long
37 :No locks available
38 :Function not implemented
39 :Directory not empty
40 :Too many levels of symbolic links
41 :Unknown error 41
42 :No message of desired type
43 :Identifier removed
44 :Channel number out of range
45 :Level 2 not synchronized
46 :Level 3 halted
47 :Level 3 reset
48 :Link number out of range
49 :Protocol driver not attached
50 :No CSI structure available
51 :Level 2 halted
52 :Invalid exchange
53 :Invalid request descriptor
54 :Exchange full
55 :No anode
56 :Invalid request code
57 :Invalid slot
58 :Unknown error 58
59 :Bad font file format
60 :Device not a stream
61 :No data available
62 :Timer expired
63 :Out of streams resources
64 :Machine is not on the network
65 :Package not installed
66 :Object is remote
67 :Link has been severed
68 :Advertise error
69 :Srmount error
70 :Communication error on send
71 :Protocol error
72 :Multihop attempted
73 :RFS specific error
74 :Bad message
75 :Value too large for defined data type
76 :Name not unique on network
77 :File descriptor in bad state
78 :Remote address changed
79 :Can not access a needed shared library
80 :Accessing a corrupted shared library
81 :.lib section in a.out corrupted
82 :Attempting to link in too many shared libraries
83 :Cannot exec a shared library directly
84 :Invalid or incomplete multibyte or wide character
85 :Interrupted system call should be restarted
86 :Streams pipe error
87 :Too many users
88 :Socket operation on non-socket
89 :Destination address required
90 :Message too long
91 :Protocol wrong type for socket
92 :Protocol not available
93 :Protocol not supported
94 :Socket type not supported
95 :Operation not supported
96 :Protocol family not supported
97 :Address family not supported by protocol
98 :Address already in use
99 :Cannot assign requested address
100 :Network is down
101 :Network is unreachable
102 :Network dropped connection on reset
103 :Software caused connection abort
104 :Connection reset by peer
105 :No buffer space available
106 :Transport endpoint is already connected
107 :Transport endpoint is not connected
108 :Cannot send after transport endpoint shutdown
109 :Too many references: cannot splice
110 :Connection timed out
111 :Connection refused
112 :Host is down
113 :No route to host
114 :Operation already in progress
115 :Operation now in progress
116 :Stale file handle
117 :Structure needs cleaning
118 :Not a XENIX named type file
119 :No XENIX semaphores available
120 :Is a named type file
121 :Remote I/O error
122 :Disk quota exceeded
123 :No medium found
124 :Wrong medium type
125 :Operation canceled
126 :Required key not available
127 :Key has expired
128 :Key has been revoked
129 :Key was rejected by service
130 :Owner died
131 :State not recoverable
132 :Operation not possible due to RF-kill
133 :Memory page has hardware error
134 :Unknown error 134
135 :Unknown error 135
136 :Unknown error 136
137 :Unknown error 137
138 :Unknown error 138
139 :Unknown error 139

这些都是我们的退出码代表的,我们来看看我们的代码是怎么写的。

#include <stdio.h>
#include <string.h>
int main()
{
//  printf("hello word\n");
//  return 0;
  int i = 0;
  for(i = 0; i < 255; i++)
  {
    printf("%d :%s\n",i,strerror(i));

  }

    return 0;
}

所以通过打印退出码所对应的信息我们是不难发现0就是代表成功的意思,非0就是代表失败的意思,从1 ——139都有对应的错误对应信息。这些信息就是来提示用户你这里是有问题的。所以这也就是为什么父进程bash需要获取子进程的退出码的原因了。

那我们今天还是需要来了解kill的一些选项,也结合退出码更好的来判断信号。但是我们这里只是看效果,并不是学信号,信号的部分后面是会继续分享给大家的。

这个时候如果再来看我们的退出码就已经是没有用了,因为这个时候不是合理退出,我们的代码是没有跑完的,我们代码写的内容其实就是一个死循环,里面打印的就是当前进程的pid,我们来看看我们的代码然后在进行解释。

这个时候我们称作为异常,代码没有跑完,我们的程序就已经进行退出的时候,这个时候就称作为异常。

我们之前经常再语言层面对于异常来说的时候,就是指我们的程序崩溃了,但是现在在我们的操作系统的层面的时候我们其实就是可以认为进程被操作系统杀掉了。因为我们的进程做了不应该做的事,然后被操作系统知道之后进行拦截了。

如果大家在牛客网上之前是写过代码的话,有时候报错的时候就会显示段错误,这个时候也就是我们的进程做了不该做的事情导致的段错误,这个时候我们的退出码就是没有意义了。

退出码有的时候也不一定是操纵系统给我们安排的,因为我们也可以使用自定义的退出码。

所以我们看我们进程退出的时候,可以先来看我们的进程信号,先检查是否是异常导致的,然后再来看我们的退出码。

总结 :衡量一个进程是否正确,只需要来看它的退出信号和退出码就可以了。

再来分享一个知识点,就是我们的子进程再退出的时候,我们是先把它的代码和数据销毁,然后在是内核数据的结构,这个时候,我们的子进程处于一种僵尸进程,他是需要等待父进程来进行回收的,所以就是会保留退出码和退出信号,等着父进程来进行回收的。

如何终止我们的进程??

我们可以用C语言的库函数来进行终止我们的程序。

exit函数

这就是我们exit函数,中间的参数就是我们的退出码,没错,因为退出码是可以自定义的,所以我们也能进行对我们的退出码进行修改。

exit表示进程的终止。直接退出。

下面来看看代码和具体效果。

那还是有一个函数就是_exit,其实他们两个没有什么不同,唯一不同的就是_exit是系统接口。我们来继续写一个函数可以来对比一些下他们的不一样的地方。

 这两个图其实就是可以说明了,因为我们的代码没有加上\n,所以是不会刷新缓冲区的,但是等到代码结束的时候就是会强制刷新的,然后_exit是没有进行刷新,(因为我们之前也是讲过我们的不能直接访问操作系统,而是会通过系统接口进行访问的,然后库函数exit其实还是会去调用_exit这个系统接口的),那么这里其实就是告诉大家我们的内存不是在系统中的,在哪里后面继续讲。先埋下伏笔。

进程等待

我们上来就给出结论,结论就是我们的父进程是会等待子进程的退出的!!!!

如果子进程退出的时候,父进程不进行等待,就会导致子进程一直处于僵尸进程,我们知道僵尸进程是会一直存在的,我们也不能用kill进行杀掉,僵尸进程需要等待父进程来进行回收,父进程回收的话需要收集子进程的退出信息。

那么我们为什么要进行进程等待呢,如果不发生进程等待是会发生什么呢?

父进程通过等待就可以解决子进程的信息,会进行资源的回收(一定要进行考虑的)

获取子进程的退出信息(退出信号和退出码)(不一定要进行考虑的)

那我们这里需要来了解系统调用的函数,分别是wait/waitpid,我们先来看看wait,但是我们主要还是看waitpid。先来写我们的代码。

可以看到我们的效果是这样的,的确发现父进程就是在等待我们的子进程,而且等待过程中子进程是处于僵尸进程。

#include <stdio.h>
#include <unistd.h>
#include<stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/wait.h>
void ChildRun()
{
    int cnt = 5;
    while(cnt)
    {
        printf("I am child process, pid: %d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
        sleep(1);
        cnt--;
    }
}

int main()
{
    printf("I am father, pid: %d, ppid:%d\n", getpid(), getppid());

    pid_t id = fork();
    if(id == 0)
    {
        // child
        ChildRun();
        printf("child quit ...\n");
        exit(123);
    }
    sleep(7);
    // fahter
    pid_t rid = wait(NULL);
//     int status = 0;
   // pid_t rid = waitpid(id, &status, 0);
    if(rid > 0)
    {
        printf("wait success, rid: %d\n", rid);
    }
    else
    {
        printf("wait failed !\n");
    }
    sleep(3);
    return 0;
}

所以这就是wait的用法,这个就可以说明我们的父进程是会等待子进程的,这里我们需要再进一步的解释一个问题,就是父进程在等待子进程的过程中,父进程是一直等待的,只有等子进程结束之后,那么父进程也就真正的结束掉了,因为父进程是需要收集子进程的退出信息的。

下面我们就来解释一下waitpid是个怎么样子的。

我们可以来看看waitpid的参数

第一个其实是子进程的pid,第二个就是我们的输出型的参数,是和我们之前认识scanf函数是一样的。然后就是来看看使用。

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

void ChildRun()
{
    int *p = NULL;
    int cnt = 5;
    while(1)
    {
        printf("I am child process, pid: %d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
        sleep(1);
        cnt--;
        *p = 100;
    }
}

int main()
{
    printf("I am father, pid: %d, ppid:%d\n", getpid(), getppid());

    pid_t id = fork();
    if(id == 0)
    {
        // child
        ChildRun();
        printf("child quit ...\n");
        exit(123);
    }
    sleep(7);
    // fahter
    //pid_t rid = wait(NULL);
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if(rid > 0)
    {
        printf("wait success, rid: %d\n", rid);
    }
    else
    {
        printf("wait failed !\n");
    }
    sleep(3);
    printf("father quit, status: %d, child quit code : %d, child quit signal: %d\n", status, (status>>8)&0xFF, status & 0x7F);
}

 

wait waitpid ,都有一个 status 参数,该参数是一个输出型参数,由操作系统填充。
如果传递 NULL ,表示不关心子进程的退出状态信息。
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status 不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究 status 16 比特 位)
那今天分享的内容就到这里,我们下次再见

 

  • 69
    点赞
  • 71
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 39
    评论
评论 39
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

在冬天去看海

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值