在linux下,csapp.h和csapp.c要自己导入,放到 /usr/include的文件夹里面,并编辑csapp.h,在#end if前面加上一句#include
因为csapp.c中有关于线程的头文件,在用gcc的时候最后要加上-lpthread
父进程通过调用fork函数来创建一个新的运行的子进程!
新创建的子进程几乎但不完全与父进程相同。新创建的子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用Fork函数时,子进程可以读写父进程中打开的任何文件。父进程和子进程最大的区别在于他们有不同的PID(PID是指process id 即进程标识符)。
区分程序是在父进程还是子进程中的方法是:
Fork函数只被一次调用却会有两次返回:一次是在调用父进程中,一次是在新创建的子进程中。在父进程中,Fork函数返回子进程的PID(大于0的数);在子进程中,Fork函数返回0。
附百度百科:
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
引入进程是为了给应用程序提供以下两方面的抽象:
1.一个独立的逻辑控制流。每个进程拥有一个独立的逻辑控制流,使得程序员以为自己的程序在执行过程中独占使用处理器。
2.一个私有的虚拟地址空间。每个进程拥有一个私有的虚拟地址空间,使得程序员以为自己的程序在执行过程中独占使用存储器。
接下来我给的例子我都会借助进程图来辅助说明:
进程图是刻画程序语句的偏序的一种简单的前趋图。每个顶点a对应于一条程序语句的执行。有向边a->b表示语句a发生在语句b之前。边上可以标注一些信息,例如一个变量的当前值。对应于printf语句的顶点可以标记上printf的输出(这里输出的都是hello便没有标记)。每张图从一个顶点开始,对应于调用main的父进程。这个顶点没有入边并且只有一个出边。每个进程顶点序列结束于一个对应exit调用的顶点。这个顶点只有一条入边没有出边。
#include "csapp.h"
/* $begin fork */
/* $begin wasidefork */
int main(int argc, char *argv[])
{
pid_t pid;
int x = 1;
pid = Fork(); //line:ecf:forkreturn
if (pid == 0) { /* Child */
printf("child : x=%d\n", ++x); //line:ecf:childprint
fflush(stdout);
return 0;
}
/* Parent */
printf("parent: x=%d\n", --x); //line:ecf:parentprint
fflush(stdout);
return 0;
}
/* $end fork */
/* $end wasidefork */
运行命令:
gcc -o runfork fork.c csapp.c -lpthread
./runfork
parent: x=0
child : x=2
#include "csapp.h"
#include
#include
#include
int main()
{
int x = 3;
if (Fork() != 0)
printf("x=%d\n", ++x);
printf("x=%d\n", --x);
exit(0);
}
运行命令:
gcc -o forkprob3 forkprob3.c csapp.c -lpthread
进程图:
结果:
根据父子进程并发运行的关系,再结合进程图,此程序还有一种结果(但是我一直没有跑出来,借用“安弦”的博客效果图)
对于有两个结果的原因:
1.并发:交替运行
2.并行:同一时刻,两个或多个都在执行
fork函数产生的子进程和父进程是并发运行的,内核能够以任何方式交替执行它们的逻辑控制流中的指令。我们不能对不同进程中指令的交替执行做任何的假设。但是可以干预它们执行的顺序,可用sleep函数让父进程或子进程休眠一会或者用wait函数
#include "csapp.h"
int main()
{
int i;
for(i=0;i<2;++i)
Fork();
printf("hello\n");
exit(0);
}
运行命令:
gcc -o forkprob1 forkprob1.c csapp.c -lpthread
#include "csapp.h"
void doit()
{
Fork();
Fork();
printf("hello\n");
return;
}
int main()
{
doit();
printf("hello\n");
exit(0);
}
运行命令:
gcc -o forkprob4 forkprob4.c csapp.c -lpthread
结果:
共有八个hello输出
进程图:
入手题目之前要先知道return与exit的区别:
此区别转载于下面的链接,更多详情可参考下面的链接:
链接:https://blog.csdn.net/firefly_2002/article/details/7960595
3. 8.14 forkprob5.c
/* $begin forkprob5 */
#include "csapp.h"
void doit()
{
if (Fork() == 0) {
Fork();
printf("hello\n");
exit(0);
}
return;
}
int main()
{
doit();
printf("hello\n");
exit(0);
}
/* $end forkprob5 */
运行命令:
gcc -o forkprob5 forkprob5.c csapp.c -lpthread
/* $begin forkprob6 */
#include "csapp.h"
void doit()
{
if (Fork() == 0) {
Fork();
printf("hello\n");
return;
}
return;
}
int main()
{
doit();
printf("hello\n");
exit(0);
}
/* $end forkprob6 */
运行命令:
gcc -o forkprob6 forkprob6.c csapp.c -lpthread
5 8.16 forkprob7.c
/* $begin forkprob7 */
#include "csapp.h"
int counter = 1;
int main()
{
if (fork() == 0) {
counter--;
exit(0);
}
else {
Wait(NULL);
printf("counter = %d\n", ++counter);
}
exit(0);
}
/* $end forkprob7 */
运行命令
gcc -o forkprob7 forkprob7.c csapp.c -lpthread
结果:
进程图:
这里涉及到回收进程的问题:
引用:CSDN博主「马怡青」的原创文章
1).waitpid(pid_t pid,int *statusp,int options)
pid:
pid>0时只等待进程ID为pid的子进程结束
pid=-1时等待其所有的子进程中的任何一个(只要一个)结束
options:
options=0(默认情况)时,挂起父进程,等待其子进程结束。返回子进程编号。
options=WNOHANG时,父进程不挂起。如果一个子进程都没有结束的话,返回0;否则返回子进程编号。
如果调用函数的进程没有子进程,waitpid返回-1,errno设为ECHILD
2).wait(&status)
等价于waitpid(-1,&status,0)
3).WIFEXITED(status):
如果子进程是以exit或者return正常退出的,函数返回值就为true
4).WEXITSTATUS(status)
前提是WIFEXITED一定为true,此函数返回正常终止的子进程的退出状态,即exit的值
5)atexit()
在进程结束调用exit时,调用atexit()括号中的注册函数,注册几次就调用几次。并且它的调用顺序和登记顺序是相反的。与压栈顺序有关。
/* $begin forkprob2 */
#include "csapp.h"
void end(void)
{
printf("2"); fflush(stdout);
}
int main()
{
if (Fork() == 0)
atexit(end);
if (Fork() == 0){
printf("0");fflush(stdout);
}
else{
printf("1");fflush(stdout);
}
exit(0);
}
/* $end forkprob2 */
结果:
可能输出就是拓扑排序,所以有上图给出的可能(但不止)
101202,112021
进程图:
(此进程图的输出应该实在fflush之后,故图中有错误)
7 关于atexit()函数
#include "csapp.h"
#include
#include
#include
void cleanup(void) {
printf("Cleaning up\n");
}
int main()
{
atexit(cleanup); //atexit注册了cleanup函数
fork();
exit(0);
}
结果:
正如前面所说,atexit()函数是用来调用终止函数的,在此程序中执行到atexit()函数时并不会立即输出cleanup,因为这里还没有终止函数,所以会执行下一条语句,(fork函数),然后fork子进程与父进程都会输出clesnup语句(先注册后调用)。
再看一个atexit,理解一下先注册后调用
#include "csapp.h"
#include
#include
#include
void func1(){
printf("fun1\n");
}
void func2(){
printf("fun2\n");
}
void func3(){
printf("fun3\n");
}
void func4(){
printf("fun4\n");
}
void func5(){
printf("fun5\n");
}
int main(int argc,char *argv){
atexit(func1);
atexit(func2);
atexit(func3);
atexit(func4);
atexit(func5);
sleep(2);
printf("main\n");
return 0;
}
运行命令:
gcc -o cm_atexit cm_atexit.c -lpthread
./cm_atexit
结果:
当执行到return 0 时,exit会自动调用这些已注册过的函数,但是由于压栈过程中先入后出的原则,所以先注册的函数最后执行。
#include "csapp.h"
int main()
{
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 */ //父进程陷入死循环
}
}
运行命令:
gcc -o fork9 fork9.c csapp.c -lpthread
./fork9
结果:
###当执行到while(1)时,程序陷入死循环,按Ctrl+z(挂起)或Ctrl+c(强行终止)
###可以用ps查看当前进程
10
#include "csapp.h"
int main()
{
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);
}
}
这个函数与上面不同的点在于while(1)的位置不同,这里的父进程是在shell main创建的,父进程结束自然回到shell命令行中。上面的程序,父进程在死循环中出不来故一直不能出现shell命令行!
结果:
11.一道有关fork和缓冲区的企业面试题
1)代码一:
int main(void)
{
int i;
for(i=0; i<2; i++){
fork();
printf("*");
}
return 0;
}
由前面给的代码,你可能认为这里还是输出6可*,其实不然,这里的输出语句,没有\n,printf若没有遇到\n(换行符),就会将输出存储在缓冲区,并没有实际的写到屏幕上,前面提到用fork函数创建子进程时,子进程能够得到父进程的代码和数据段、堆、共享库以及用户栈。所以把缓冲区里的数据也复制到子进程里
结果:
要想要解决缓冲区这个问题 :
1.加上换行符\n,换行符能够输出行冲区里的数据并刷新缓冲区;
2.使用fflush函数去清空缓冲区;
12.不算main自身这进程,这个程序到底创建了多少个进程
int main(int argc,char *argv[])
{
Fork();
Fork()&&Fork()||Fork();
Fork();
}
先加一些辅助性的输出,跑一遍,看结果:
#include "csapp.h"
int main(int argc,char *argv[])
{
Fork();
Fork()&&Fork()||Fork();
Fork();
printf("*\n");
}
结果:
加上main自身输出的*,一共有20个*,所以除去main本身,共产生了19个进程
分析:
关键之处在于Fork()&&Fork()||Fork();这个语句。
对于//A&&B||c这个格式的语句,有以下几种执行情况
对于&&连接的两个值,若前面为假,表达式的结果就为假,则不判断后面的真假情况,
对于||连接的两个值,若前面为真,则表达式就为真,则不判断后面的真假情况
所以有:
1)A为假时,B不执行,C执行
2)A为真,B为假,执行C
2)A为真,B为真,不执行C
进程图:
参考链接:
https://www.cnblogs.com/love-jelly-pig/p/8471206.html
https://blog.csdn.net/weixin_43329358/article/details/102932891
https://blog.csdn.net/weixin_44688476/article/details/102868173