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

目录

一、进程创建

fork函数

写时拷贝

二、进程终止

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

进程退出场景

常见信号码及其含义

进程退出的常见方法

正常终止与异常终止

exit与_exit的区别


一、进程创建

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(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() 调用后,不需要立即复制父进程的所有资源,减少了不必要的内存消耗和复制时间。

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

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

缺点:

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

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


 

二、进程终止

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

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

进程退出场景
  • 代码执行完毕,结果正确

#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;
}
[wuxu@Nanyi lesson16]$ gcc -o test test1.c -std=c99
[wuxu@Nanyi lesson16]$ ./test
Add 1 to 100 is 5050
  • 代码运行完毕,结果不正确

#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;
}
[wuxu@Nanyi lesson16]$ gcc -o test test1.c -std=c99
[wuxu@Nanyi lesson16]$ ./test
Add 1 to 100 is 4950
  • 代码异常终止。即代码没有跑完,程序崩溃

#include <stdio.h>

int main()
{
        int* p = NULL;
        *p = 100;//空指针解引用--->野指针
        return 0;
}
[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)获取错误码的错误描述

#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 $?来查看最近一次执行的子进程的退出码

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

[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的程序,看会出现什么错误

#include <stdio.h>

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

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

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

#include <stdio.h>

int main()
{
	while(1)
	{}
	return 0;
}

 

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

进程退出的常见方法

正常终止与异常终止

正常终止(可以通过 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;
}
[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

[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的参数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值