【Linux进程控制】进程创建|终止

目录

一、进程创建

fork函数

写时拷贝

二、进程终止

想明白:终止是在做什么?

进程退出场景

常见信号码及其含义

进程退出的常见方法

正常终止与异常终止

exit与_exit的区别


一、进程创建

fork函数

在Linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,原进程为父进程,其中返回值:子进程中返回0,父进程返回子进程id,出错返回-1;

测试

cpp 复制代码
#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(PPID)以及一些独立的资源,如虚拟内存等。

3.执行流程fork() 调用之后,父进程和子进程都会从 fork() 函数调用后的下一条指令开始执行。

4.返回值fork() 在父进程中返回子进程的 PID,在子进程中返回 0,如果出错则返回 -1。

fork常规用法:

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求

  • 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数

fork调用失败的原因

  • 系统中有太多的进程

  • 实际用户的进程数超过了限制

写时拷贝

写时拷贝(Copy-on-Write,简称COW)是一种计算机程序设计的优化策略。这种策略在多个进程试图写入同一块数据时,才会真正进行数据复制,而不是一开始就为每个进程分配独立的物理内存空间。

工作原理:

1.共享数据 :当父进程通过 fork() 创建子进程时,并不立即为子进程分配一份父进程数据段的副本。相反,父子进程共享同一块物理内存页。

2.写操作检测:操作系统会标记这些共享的内存页为"写时拷贝"。这意味着如果任何一个进程试图写入这些页,操作系统会捕捉到这个写操作。

3.数据复制:当写操作发生时,操作系统会触发页错误(page fault)。操作系统随后会创建一个新的内存页,并将原页的内容复制到新页上,然后将写操作指向新的内存页。对于其他进程,原内存页保持不变。

4.页分离:这个过程称为页分离(page splitting)。之后,每个进程都会有自己的内存页副本,对其中一个进程的修改不会影响到另一个进程。

优点:

  • 效率提升 :在 fork() 调用后,不需要立即复制父进程的所有资源,减少了不必要的内存消耗和复制时间。

  • 内存使用优化:只有在实际需要时才分配内存,这可以显著减少内存的使用。

  • 性能提升:减少了进程创建时的开销,提高了系统的整体性能。

缺点:

  • 写操作开销:第一次写操作时会有额外的开销,因为需要复制内存页。

  • 复杂性:实现写时拷贝会增加操作系统内核的复杂性。

二、进程终止

想明白:终止是在做什么?

操作系统要释放进程申请的相关内核数据结构和对应的数据和代码(本质就是释放系统资源)。

进程退出场景
  • 代码执行完毕,结果正确
cpp 复制代码
#include <stdio.h>
int Add(int from, int to)
{
	int sum = 0;
	for(int i = from; i <= to; i++)
	{
		sum += i;
	}
	return sum;
}
int main()
{
	printf("Add 1 to 100 is %d\n", Add(0, 100));
	return 0;
}
cpp 复制代码
[wuxu@Nanyi lesson16]$ gcc -o test test1.c -std=c99
[wuxu@Nanyi lesson16]$ ./test
Add 1 to 100 is 5050
  • 代码运行完毕,结果不正确
cpp 复制代码
#include <stdio.h>
int Add(int from, int to)
{
        int sum = 0;
  //此处应该是 i<=to
        for(int i = from; i < to; i++)
        {
                sum += i;
        }
        return sum;
}

int main()
{
        printf("Add 1 to 100 is %d\n", Add(0, 100));
        return 0;
}
cpp 复制代码
[wuxu@Nanyi lesson16]$ gcc -o test test1.c -std=c99
[wuxu@Nanyi lesson16]$ ./test
Add 1 to 100 is 4950
  • 代码异常终止。即代码没有跑完,程序崩溃
cpp 复制代码
#include <stdio.h>

int main()
{
        int* p = NULL;
        *p = 100;//空指针解引用--->野指针
        return 0;
}
cpp 复制代码
[wuxu@Nanyi lesson16]$ gcc -o test test2.c -std=c99
[wuxu@Nanyi lesson16]$ ./test
Segmentation fault
  • 在程序执行结束时,我们会使用return语句返回一个数值作为main函数的返回值,这个返回值有什么用呢?

【例子1】张三参加一场考试,回家后给老爹汇报成绩

如果小明考了100分(满分100)那么他的老爹并不会关心他为什么考了100分;但当小明考了1分,他的老爹则会问他为什么考1分。因此做出如下约定,每个数字标识不同的原因:

状态码 描述
1 考试过程中生病了导致没考好
2 没有好好学习导致没考好
... ...

在操作系统中,对于程序正常终止我们并不关心(正常程序终止返回状态码0),但程序一旦出现错误(返回码非0),我们就需要知道程序出错的原因。操作系统对于不同的状态码给了不同的错误描述信息,我们可以使用errno.h 下的 errno 变量获取错误码,使用 strerror(errno)获取错误码的错误描述

cpp 复制代码
#include <stdio.h>
#include <string.h>

int main()
{
	for(int i = 0; i < 200; i++)
	{
		printf("[%d]->%s\n", i, strerror(i));
	}
	return 0;
}
  • 谁会关心当前进程的退出码呢?-->父进程

父进程为何关心子进程的退出码?

错误处理:如果子进程因为错误而终止,它通常会返回一个非零的退出码。父进程可以根据这个退出码来决定是否需要采取补救措施,比如重新执行失败的子进程,或者记录错误信息。

流程控制:在某些情况下,父进程的后续行为可能依赖于子进程的成功执行。如果子进程返回一个表示成功的退出码(通常是0),父进程可以继续执行下一步操作;否则,它可能会停止执行或执行不同的代码路径。

状态报告:父进程可能需要向用户或其他进程报告子进程的执行结果。退出码是传递这种状态信息的简单方式。

在Linux中,可以使用echo $?来查看最近一次执行的子进程的退出码

我们在回到刚刚野指针的例子上,重新执行一下程序:

cpp 复制代码
[wuxu@Nanyi lesson16]$ gcc -o test test2.c -std=c99
[wuxu@Nanyi lesson16]$ ./test
Segmentation fault
[wuxu@Nanyi lesson16]$ echo $?
127
[wuxu@Nanyi lesson16]$ echo $?
0

通过观察我们发现同一个程序,为什么两次的退出码不一样?

其实第一个127是./test的执行码,表示这个程序出现了Segmentation fault错误,第二个执行码表示echo这个命令执行成功,返回0

一旦程序出现异常,退出码就没有意义了

为什么出现了异常?--> 我们可以看进程退出的时候,退出信号是多少,就可以判断进程为什么异常了。

进程出异常本质是因为进程收到了OS发给进程的信号

【示例】我们写一个除0的程序,看会出现什么错误

cpp 复制代码
#include <stdio.h>

int main()
{
	int a = 1 / 0;
	return 0;
}
cpp 复制代码
[wuxu@Nanyi lesson16]$ ./error
Floating point exception

在该程序发生错误时,操作系统给该程序的进程发送了8号信号SIGFPE。我们可以通过 kill -l 查看所有的信号码以及对应信号名

我们来验证一下,上面的程序时接收到8号信号才终止的

cpp 复制代码
#include <stdio.h>

int main()
{
	while(1)
	{}
	return 0;
}
常见信号码及其含义
信号码 信号名称 含义
1 SIGHUP 挂起,通常在终端关闭或控制进程结束时发送给子进程。
2 SIGINT 中断,通常在用户按下Ctrl+C时发送。
3 SIGQUIT 退出,用户按下Ctrl+\时发送,通常会导致进程终止并生成核心转储。
4 SIGILL 非法指令,执行了非法的机器语言指令。
5 SIGTRAP 跟踪陷阱,由调试器使用。
6 SIGABRT 中止,调用abort()函数时发送。
7 SIGBUS 总线错误,涉及硬件错误。
8 SIGFPE 浮点异常,如除以零。
9 SIGKILL 杀死,无法捕获、阻塞或忽略,总是终止进程。
10 SIGUSR1 用户定义的信号1,可用于应用程序。
11 SIGSEGV 段违例,访问非法内存地址。
12 SIGUSR2 用户定义的信号2,可用于应用程序。
13 SIGPIPE 管道破裂,写入无读者的管道时发生。
14 SIGALRM 报警,由alarm()函数设置的时间到期时发送。
15 SIGTERM 终止,请求进程终止。
信号码 信号名称 含义
16 SIGSTKFLT 栈溢出(Linux特有,在一些系统中不存在)
17 SIGCHLD 子进程结束,子进程处于停止状态或被终止时发送给父进程。
18 SIGCONT 继续执行,如果进程已停止,则使其继续运行。
19 SIGSTOP 停止进程的执行,无法被捕获或忽略。
20 SIGTSTP 停止进程的执行,可以被捕获,通常在用户按下Ctrl+Z时发送。
21 SIGTTIN 后台进程组尝试读取控制终端时发送。
22 SIGTTOU 后台进程组尝试写入控制终端时发送。
23 SIGURG I/O紧急情况,套接字有紧急数据可读。
24 SIGXCPU 超过CPU时间限制(CPU时间限制超时)。
25 SIGXFSZ 超过文件大小限制。
26 SIGVTALRM 虚拟定时器警报(类似于SIGALRM,但是计算的是进程的虚拟时间)。
27 SIGPROF 性能计数器超时(类似于SIGALRM,但是包括了处理器时间和时钟时间)。
28 SIGWINCH 窗口大小改变,通常在终端窗口大小改变时发送。
29 SIGIO I/O可执行(Solaris系统中为SIGPOLL)。
30 SIGPWR 电源故障(系统关机)。
31 SIGSYS 系统调用异常(无效的系统调用)。

进程退出的常见方法

正常终止与异常终止

正常终止 (可以通过 echo $? 查看进程退出码)

  • 从main函数返回

  • 调用exit

  • _exit

异常终止

  • ctrl + c 信号终止

exit与_exit的区别

  • 终止处理程序和I/O缓冲区exit()会执行终止处理程序和I/O缓冲区的清理,而_exit()则不会。

  • 头文件exit()stdlib.h中定义,而_exit()unistd.h中定义。

  • 用途 :由于_exit()不会进行清理工作,它通常用于不需要这些清理步骤的底层系统编程。

【例子】

cpp 复制代码
#include <stdio.h>
#include <unistd.h>

int main()
{
        printf("1 + 1 = %d", 1 + 1);
        _exit(1);
        return 0;
}
cpp 复制代码
[wuxu@Nanyi lesson16]$ vim test5.c
[wuxu@Nanyi lesson16]$ gcc -o test test5.c -std=c99
[wuxu@Nanyi lesson16]$ ./test
[wuxu@Nanyi lesson16]$ echo $?
1

通过结果我们发现,并没有打印1+1=2这个结果,也就是_exit不会刷新缓冲区,故最后并没有打印。

如果换成exit

cpp 复制代码
[wuxu@Nanyi lesson16]$ gcc -o test test5.c -std=c99
[wuxu@Nanyi lesson16]$ ./test
1 + 1 = 2 [wuxu@Nanyi lesson16]$ echo $?
1

我们会发现它打印出最终结果,顺便提醒一下 exit与_exit 头文件不一样哦

exit最后也会调用_exit,但在exit除了调用_exit,还做了其他工作:

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

❍ 关闭所有打开的流,所有的缓存数据均被写入(即刷新缓冲区)

​ ❍ 再调用_exit

return退出

return是一种更常见的退出进程方法。执行return n 等同于执行exit(n),因为调用main的运行时函数会将main的返回值当作exit的参数

相关推荐
hjjdebug1 小时前
linux 下 signal() 函数的用法,信号类型在哪里定义的?
linux·signal
其乐无涯1 小时前
服务器技术(一)--Linux基础入门
linux·运维·服务器
Diamond技术流1 小时前
从0开始学习Linux——网络配置
linux·运维·网络·学习·安全·centos
斑布斑布1 小时前
【linux学习2】linux基本命令行操作总结
linux·运维·服务器·学习
Spring_java_gg1 小时前
如何抵御 Linux 服务器黑客威胁和攻击
linux·服务器·网络·安全·web安全
✿ ༺ ོIT技术༻1 小时前
Linux:认识文件系统
linux·运维·服务器
会掉头发2 小时前
Linux进程通信之共享内存
linux·运维·共享内存·进程通信
我言秋日胜春朝★2 小时前
【Linux】冯诺依曼体系、再谈操作系统
linux·运维·服务器
饮啦冰美式3 小时前
22.04Ubuntu---ROS2使用rclcpp编写节点
linux·运维·ubuntu