网站开发工程师考试,百度云服务器wordpress,新乡网站建设设计,教育局网站建设方案进程创建
初始fork函数
fork函数是为了创建子进程而生的#xff0c;通过fork函数之后#xff0c;我们的父进程的代码和数据是共享的#xff0c;我们这里是可以通过man手册进行查询的#xff0c;查询之后是可以发现fork函数是会返回两个值的至于为什么会返回两个值#x… 进程创建
初始fork函数
fork函数是为了创建子进程而生的通过fork函数之后我们的父进程的代码和数据是共享的我们这里是可以通过man手册进行查询的查询之后是可以发现fork函数是会返回两个值的至于为什么会返回两个值我们下面来进行探讨。
首先在我们之前的文章里讲过fork函数是会进行返回两个值的这个值相当于给父进程进行写入的。而且我们如果通过打印的化不难发现父进程返回的值就是我们创建出来子进程的pid而我们的子进程在fork函数之后返回的其实是0我们之前解释过为什么fork函数之后是会返回两个值的。 解释原因进程其实本质上就是内核的某种数据结构地址空间mm_struct 页表 PCBLinux下是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 页表 PCBLinux下是task_struct 进程的代码和数据组成的这我们上面是讲过的但是这里我们还是需要想想我们是先将我们的代码和数据放在内存里还是先创建task_struct和地址空间和页表呢 我们可以这么想在我们上大学的时候我们填完志愿之后学校录取你之后是你马上就要去学校吗我们肯定是会在要开学的时候才是会去学校的这个时候我们的信息是不是已经在学校里了但是我们人还是不需要去学校的所以我们的进程就是先创建PCB和地址空间还有我们的页表。然后才是我们程序的代码和数据。 那话又说回来进程终止到底是在干嘛呢答案就是先释放我们的代码和数据然后就是我们进程的内核的数据结构释放的顺序大家一定要记住的我们后面就会讲退出码和信号的时候就会用到。
我们先来写一下世界上最简单的代码。 这么简单的代码我肯定是故意打错的。
我们来改一下我们的代码吧.
[tjlhecs-67680 3_22]$ make
gcc -o myprocess myprocess.c
[tjlhecs-67680 3_22]$ ./myprocess
hello word
[tjlhecs-67680 3_22]$ 看完操作之后来看看我们的代码
#include stdio.h
int main()
{printf(hello word\n);return 0;
}接下来我们需要思考的就是我们为什么每次代码都需要return 0 为什么不是return 100 这些难道它这个返回值也是有不同的意义的。 进程的退出码
我们在我们主函数程序结束的时候会返回一个值这个值就是我们的退出码。
退出码是很重要的它主要就是给我们的父进程bash来看的这也就有有了我们上面进程终止的三种情况。 第一种 代码正常运行 代码跑完结果也是正确的
第二种代码运行代码跑完之后但是结果是不正确的
第三种就是异常如果在我们语言方面是可以讲成程序崩溃 echo $? 我们用这个指令就是可以查询我们的退出码了。
我们之前说过ls这些简单的指令也是一个进程我们也可以查出他们的退出码。
[tjlhecs-67680 3_22]$ ls adafa
ls: cannot access adafa: No such file or directory
[tjlhecs-67680 3_22]$ echo $?
2
[tjlhecs-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
#includestdlib.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){// childChildRun();printf(child quit ...\n);exit(123);}sleep(7);// fahterpid_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.hvoid 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){// childChildRun();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, (status8)0xFF, status 0x7F);
} wait 和 waitpid 都有一个 status 参数该参数是一个输出型参数由操作系统填充。 如果传递 NULL 表示不关心子进程的退出状态信息。 否则操作系统会根据该参数将子进程的退出信息反馈给父进程。 status 不能简单的当作整形来看待可以当作位图来看待具体细节如下图只研究 status 低 16 比特 位 那今天分享的内容就到这里我们下次再见