我与Linux的爱恋:进程创建|终止
🔥个人主页:guoguoqiang. 🔥专栏:Linux的学习
文章目录
- 一、进程创建
- **fork函数**
- 写时拷贝
- 二、进程终止
- 进程退出的常见方法
一、进程创建
fork函数
在Linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,原进程为父进程,其中返回值:子进程中返回0,父进程返回子进程id,出错返回-1;
测试
#include <stdio.h>
#include <assert.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
printf("before fork, pid = %d\n", getpid());
pid_t id = fork();
assert(id != -1);//进程创建失败
(void)id;
printf("after fork, pid = %d, fork return %d\n", getpid(), id);
return 0;
}
上面代码执行路径如下图所示
进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
当父进程调用fork时,会发生:
1.进程复制: 操作系统会创建父进程的一个副本,这个副本就是子进程。子进程几乎和父进程完全相同。
2.资源共享与复制 子进程与父进程有区别 ,例如 他们有不同的进程ID(PID).不同的父进程ID(PID)以及一些独立的资源,如虚拟内存等。
3.执行流程:fork() 调用之后,父进程和子进程都会从 fork() 函数调用后的下一条指令开始执行。
4.返回值:fork() 在父进程中返回子进程的 PID,在子进程中返回 0,如果出错则返回 -1。
fork的常规用法
一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如:父进程等待请求,子进程执行请求。
一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
fork调用失败的原因:
系统中有太多的进程
实际用户的进程超过了限制
写时拷贝
写时拷贝(Copy-on-Write,COW)是一种优化技术,主要用于管理内存和资源,尤其在操作系统和数据库系统中广泛应用。
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。
工作原理
1.初始状态:
当一个对象或数据被复制时,系统并不立即创建一个新的实例,而是创建一个指向原始数据的引用或指针。
2.共享数据:
多个对象或进程可以共享同一份数据,直到其中一个对象需要修改数据。
3.数据修改:
当某个对象尝试修改共享数据时,系统会将这份数据进行拷贝,创建一个独立于原始数据的新实例。
只有修改的数据被拷贝,而其他未被修改的部分仍然与原始数据共享,这样可以节省内存和提高效率。
4.独立性:
修改后的对象不再共享原始数据的引用,而是独立于其他对象,从而确保数据的一致性。
优点
1.内存效率:
由于不立即复制数据,减少了不必要的内存使用,可以更有效地管理系统资源。
2.性能提升:
在大多数情况下,降低了复制开销,尤其是在频繁创建和销毁对象的场景中。
3.线程安全:
每个线程或进程在写入时都拥有自己的数据副本,避免了多线程环境中潜在的数据竞争和冲突问题。
4.简化回滚操作:
在某些应用中,如数据库或版本控制系统,可以容易地回滚到之前的状态,因为未修改的部分仍然共享原始数据。
缺点
1.延迟开销:
在第一次写入时,由于涉及数据拷贝,可能会造成性能的延迟,这在某些实时系统中是不可接受的。
2.增加复杂性:
管理指针和引用关系可能增加系统的复杂性,尤其在需要频繁检查和操作这些引用时。
3.内存碎片:
在频繁拷贝和修改的情况下,可能导致内存碎片化,影响系统的整体性能。
4.不足以处理大数据:
在处理非常大的数据集时,COW可能会导致过多的拷贝和高内存占用,这会抵消其初衷。
二、进程终止
想一下进程终止是什么?
操作系统要释放进程申请的相关内核数据结构和对应的数据和代码(本质就是释放系统资源)。
进程退出场景
- 代码执行完毕,结果正确
#include <stdio.h>
int Add(int begin,int end){
int sum=0;
for(int i=begin;i<=end;i++){
sum+=i;
}
return sum;
}
int main(){
printf("Add 1 to 100 is %d\n",Add(0,100));
return 0;
}
- 代码运行完毕,结果不正确
#include <stdio.h>
int Add(int begin,int end){
int sum=0;
for(int i=begin;i<end;i++){
sum+=i;
}
return sum;
}
int main(){
printf("Add 1 to 100 is %d\n",Add(0,100));
return 0;
}
- 代码异常终止。代码没跑完,程序崩溃
#include <stdio.h>
int main(){
int *p=NULL;
*p=100;//野指针解引用
return 0;
}
在我们写代码时程序运行结束时我们会用return语句返回一个数作为main的返回值,这个返回值有什么用呢?
例子:
在我们考试后,家长问成绩,如果考的好,就不会说什么,考的不好则需要说明原因。
我们做出下列约定:
数字 | 代表的原因 |
---|---|
1 | 考试过程中生病了 |
2 | 考试的时候着急了 |
… | … |
在操作系统中,当程序正常运行时我们不说什么,(正常程序运行终止返回状态码0),但程序一旦出现错误(返回码非0),我们就需要知道出错的原因。操作系统对于不同的状态码给了不同的错误描述信息,我们可以使用errno.h 下的 errno 变量获取错误码,使用 strerror(errno)获取错误码的错误描述。 |
#include <stdio.h>
#include <string.h>
int main()
{
for(int i = 0; i < 400; i++)
{
printf("[%d]->%s\n", i, strerror(i));
}
return 0;
}
父进程会关心子进程的退出码
父进程关心子进程的退出码(即退出状态)主要是出于以下几个原因:
- 了解子进程的执行结果
退出码通常表示子进程的执行状态,父进程可以通过这个码判断子进程是成功结束(通常为0)还是发生了错误(非0值)。这对于调试和错误处理非常重要。 - 后续处理
在一些场景中,父进程根据子进程的执行结果来决定后续的操作。例如,如果子进程成功执行,父进程可能会继续进行相关工作;如果失败,父进程可能需要采取补救措施或终止工作。 - 资源管理
子进程的退出会影响系统的资源管理。父进程需要调用相应的系统调用(如 wait 或 waitpid)来收集子进程的退出状态,防止出现僵尸进程(Zombie Process),即那些已经结束但父进程未处理其状态的进程。 - 同步与协调
在多进程环境中,父进程需要对子进程的状态进行监控,以实现进程间的同步。这对于协调多个进程的工作、确保程序的正常运行很重要。 - 日志和审计
在某些情况下,父进程可能需要记录子进程的退出状态,以便后期进行审计和分析。这在系统监控和故障排查中会有所帮助。
第一个139 表示程序出现了Segmentation fault错误
而第二个零表示echo $?成功
一旦程序出现异常,退出码就没意义了
我们可以看进程退出的时候,退出信号是多少,就可以判断进程为什么异常了。
进程出异常本质是因为进程收到了OS发给进程的信号
在该程序发生错误时,操作系统给该程序的进程发送了8号信号SIGFPE。我们可以通过 kill -l 查看所有的信号码以及对应信号名
我们来验证一下,上面的程序是通过接收到8号信号才结束的
SIGHUP 1 A 终端挂起或者控制进程终止
SIGINT 2 A 键盘中断(如break键被按下)
SIGQUIT 3 C 键盘的退出键被按下
SIGILL 4 C 非法指令
SIGABRT 6 C 由abort(3)发出的退出指令
SIGFPE 8 C 浮点异常
SIGKILL 9 AEF Kill信号
SIGSEGV 11 C 无效的内存引用
SIGPIPE 13 A 管道破裂: 写一个没有读端口的管道
SIGALRM 14 A 由alarm(2)发出的信号
SIGTERM 15 A 终止信号
SIGUSR1 30,10,16 A 用户自定义信号1
SIGUSR2 31,12,17 A 用户自定义信号2
SIGCHLD 20,17,18 B 子进程结束信号
SIGCONT 19,18,25 进程继续(曾被停止的进程)
SIGSTOP 17,19,23 DEF 终止进程
SIGTSTP 18,20,24 D 控制终端(tty)上按下停止键
SIGTTIN 21,21,26 D 后台进程企图从控制终端读
SIGTTOU 22,22,27 D 后台进程企图从控制终端写
处理动作一项中的字母含义如下
A 缺省的动作是终止进程
B 缺省的动作是忽略此信号,将该信号丢弃,不做处理
C 缺省的动作是终止进程并进行内核映像转储(dump core),内核映像转储是指将进程数据在内存的映像和进程在内核结构中的部分内容以一定格式转储到文件系统,并且进程退出执行,这样做的好处是为程序员提供了方便,使得他们可以得到进程当时执行时的数据值,允许他们确定转储的原因,并且可以调试他们的程序。
D 缺省的动作是停止进程,进入停止状况以后还能重新进行下去,一般是在调试的过程中(例如ptrace系统调用)
E 信号不能被捕获
F 信号不能被忽略
进程退出的常见方法
正常终止与异常终止
正常终止(可以通过echo $?查看进程退出码)
- 从main函数返回
- 调用exit
- _exit
异常终止
ctrl + c 信号终止
exit与_exit的区别
终止处理程序和I/O缓冲区:exit()会执行终止处理程序和I/O缓冲区的清理,而_exit()则不会。
头文件:exit()在stdlib.h中定义,而_exit()在unistd.h中定义。
_exit()不会进行清理工作。
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("1 + 1 = %d", 1 + 1);
_exit(1);
return 0;
}
我们发现没有打印出1+1的结果,也就是_exit不会刷新缓冲区
如果换成exit
exit最后也会调用exit, 但在调用exit之前,还做了其他工作:
- 执行用户通过 atexit或on_exit定义的清理函数。
- 关闭所有打开的流,所有的缓存数据均被写入
- 调用_exit
return退出
return是一种更常见的退出进程方法。执行return n 等同于执行exit(n),因为调用main的运行时函数会将main的返回值当作exit的参数