Linux系统 —— 进程控制系列 - 进程的创建与终止 :fork与exit

发布于:2024-12-22 ⋅ 阅读:(14) ⋅ 点赞:(0)

新的开始

目录

1. 进程创建

1.1 再探fork函数初识

1.2 写时拷贝

2. 进程的退出

   

3. exit

3.1 exit和_exit区别

return退出


1. 进程创建

1.1 再探fork函数初识

我们之前应该聊过fork函数了,具体可以看看这篇:

  Linux系统 —— 进程系列 - 进程的概念,PCB与PID和fork_linux top adbd是什么线程-CSDN博客icon-default.png?t=O83Ahttps://blog.csdn.net/hedhjd/article/details/144299919?spm=1001.2014.3001.5501今天我们来具体聊一聊fork函数


在linux中fork函数是非常重要的函数,它从已存在进程中创建⼀个新进程。新进程为子进程,而原进程为父进程

  

fork会有两个返回值:给子进程返回0,给父进程返回子进程pid

  

#include <unistd.h>
pid_t fork(void);
返回值:子进程中返回0,⽗进程返回⼦进程pid,出错返回 - 1

当进程调用fork函数,当控制转移到内核中的fork代码后,内核应该:

   

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

   
2. 将父进程部分数据结构内容拷贝到子进程

   
3. 添加子进程到系统进程列表当中

    
4. fork返回,开始调度器调度

   

在默认情况下,父进程和子进程代码和数据都是共享的,但是如果子进程进行写入数据或者代码,那么子进程就会对代码或者数据发生对应的写实拷贝,所以以写实拷贝的方式来实现我们父子进程的独立性,这样的话互不干扰

当⼀个进程调用fork之后,就有两个⼆进制代码相同的进程。而且它们都运行到相同的地方,看如下程序

int main(void)
{
	pid_t pid;

	printf("Before: pid is %d\n", getpid());

	if ((pid = fork()) == -1)perror("fork()"), exit(1);

	printf("After:pid is %d, fork return %d\n", getpid(), pid);
	sleep(1);

	return 0;
}
运⾏结果:
[kiana@localhost ~]# . / a.out
Before : pid is 43676
After : pid is 43676, fork return 43677
After : pid is 43677, fork return 0

这⾥看到了三⾏输出,⼀⾏before,两⾏after。进程43676先打印before消息,然后它有打印after。另⼀个after消息有43677打印的。注意到进程43677没有打印before,为什么呢?如下图所示:

所以,fork之前父进程独立执⾏,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定


1.2 写时拷贝

    
一般来说,父子代码共享,父子再不写入时,数据也是共享的,当任意⼀⽅试图写⼊,便以写时拷贝的方式各⾃⼀份副本

写实拷贝其实就是子进程赋值父进程的PCB以及进程地址空间以及页表。 然后页表的映射物理内存是相同一份数据。 但是当子进程想要修改的时候, 由于进程的独立性, 子进程修改不能影响父进程的数据,那么就要发生写时拷贝(重新映射) 

意思就是说, 一开始,子进程拷贝父进程的PCB。 虚拟地址空间, 页表。 所以父子进程中地址空间中的数据段和代码段映射到物理内存也是一样的。 但是这个时候页表会让他们都为只读状态, 如果后续的过程中子进程或者父进程都是只读, 那么就没有问题。 但是只要其中一个发生修改, 那么这个进程对应的页表就会重新映射空间

    

写实拷贝的原理就是:延时申请、按需申请

因为有写时拷⻉技术的存在,所以⽗⼦进程得以彻底分离离,完成了进程独立性的技术保证!
写时拷贝,是⼀种延时申请技术,可以提⾼整机内存的使⽤率


2. 进程的退出

   

1. 代码运行完毕,结果正确

   
2. 代码运行完毕,结果不正确

   
3. 代码异常终⽌

  

   

退出分为正常退出和不正常退出,我们上面的1和2就是正常退出,3就是不正常退出

  

而运行结果也就是return某个数字或者exit某个数字, 这里的某个数字就是运行结果,也被成为退出码

  

我们可以使用echo $?来打印我们最近一个进程退出时的退出码

这个时候我们就有一个问题:为什么运行码的表征结果最终会返回给bash进程?答案是:对于进程来说, 我们的进程的父进程会关心我们进程,所以会将结果返回给bash进程

  

父进程关心子进程什么呢?答案是:父进程关心子进程退出时代码跑完之后为什么结果是不正确的

  

代码只有0是成功的, 只要是非0那么就是不正确,而不正确的非零数字, 就会代表不同的推出原因:退出码

  

都是如果是运行异常的话,那么 程序的退出码基本可以不看,因为异常就代表没有正常执行到退出程序, 这个时候退出码没有太大意义

  

如果一个程序发生了异常终止,本质上就是进程收到了对应的信号

  

如果我们没有发信号的时候, 这个程序正在正常的跑,不出意外的话这个进程是能够无限循环的, 但是一旦我们给进程发一个信号, 那么这个进程就会被检测为异常, 进而被操作系统杀掉,由此可以看出:进程的本质就是接收到了对应的信号

 


3. exit

exit就是用来终止进程的,进程终⽌的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码

  

先看一段代码:

  

  

运行结果:

  

  

我们发现,运行结果只执行到第一个printf,并没有执行第二个,因为这个代码进程跑不到第二个printf就会遇到exit直接终止

  

一个问题:我们上面的程序的退出码是什么?答案是40, 因为对于exit来说, 无论exit在什么位置, 只要exit出现, 就会终止掉调用该函数的进程

  

3.1 exit和_exit区别

  

简单来说就是exit是C语言里的,而_exit是系统的

   

 具体区分就是:

   

进程如果调用的是我们exit的接口的话,那么exit()退出时会进行资源的回收,会进行缓冲区的刷新

  

进程如果调用的是我们_exit的接口的话,那么_exit()退出时会进行资源的回收,不会进行缓冲区的刷新

  

_exit函数 

  

#include <unistd.h>
void _exit(int status);
//参数:status 定义了进程的终⽌状态,⽗进程通过wait来获取该值

虽然status是int,但是仅有低8位可以被⽗进程所用,所以_exit(-1)时,在终端执⾏$?发现
返回值是255

  

//_exit
int main()
{
	printf("hello");

	_exit(0);
}

运行结果:

  

[root@localhost linux] # . / a.out
[root@localhost linux]#

exit函数

  

#include <unistd.h>
void exit(int status);

exit最后也会调⽤_exit, 但在调⽤_exit之前,还做了其他⼯作:

  

1. 执行用户通过 atexit或on_exit定义的清理函数

  

2. 关闭所有打开的流,所有的缓存数据均被写⼊

  

3. 调⽤_exit

  

//exit
int main()
{
	printf("hello");

	exit(0);
}

运行结果:

  

[root@localhost linux] # . / a.out
[root@localhost linux]#hello

return退出

  
return是⼀种更常⻅的退出进程⽅法。执⾏return n等同于执⾏exit(n),因为调⽤main的运⾏时函数会将main的返回值当做 exit的参数


未完待续~


网站公告

今日签到

点亮在社区的每一天
去签到