总言
进程地址空间和进程控制相关介绍。
文章目录
- 总言
- 1、进程地址空间
- 1.1、程序地址空间初识
- 1.1.1、介绍程序地址空间划分及地址空间初步验证
- 1.1.2、地址空间再次综述演示
- 1.1.3、两个补充问题:
- 1.2、地址空间是什么
- 1.2.1、阶段认识一:故事引入
- 1.2.2、阶段认识二:虚拟地址和映射机制
- 1.2.3、阶段认识三:内核数据结构和相关问题解释
- 1.3、为什么要有地址空间
- 1.3.1、理由一
- 1.3.2、理由二
- 1.3.3、理由三
- 1.3.4、关于挂起的理解
- 2、进程控制
- 2.1、进程创建
- 2.1.1、fork
- 2.1.2、写时拷贝
- 2.2、进程终止
- 2.2.1、进程终止时,操作系统做了什么?
- 2.2.2、进程终止的常见方式?
- 2.2.3、如何正确的用代码终止一个进程?
1、进程地址空间
1.1、程序地址空间初识
1.1.1、介绍程序地址空间划分及地址空间初步验证
1)、验证地址空间分布(进程打印)
相关代码如下:
#include <stdio.h>
#include <unistd.h>
#include<stdlib.h>
int un_g_val;//未初始化全局数据
int g_val=100;//已初始化全局数据
int main(void)
{
printf("test:%p\n",main);//验证代码区:函数名表示其地址,函数是代码
printf("init:%p\n",&g_val);//验证全局区:已初始化全局区
printf("unint:%p\n",&un_g_val);//验证全局区:未初始化全局区
char*p=(char*)malloc(16);
printf("heap:%p\n",p);//验证堆区:malloc在堆上申请空间,将对应地址存储在p变量中,*p指向堆上的地址空间,
printf("stack:%p\n",&p);//验证栈区:p变量本身在在main函数(栈)中创建,故&p获得的是栈上的地址。
return 0;
}
验证结果如下:
2)、验证堆区地址分布(栈区地址分布在上述中已经验证)
[wj@VM-4-3-centos t1109]$ ./proc.out
test:0x40057d
init:0x60103c
unint:0x601044
heap1:0xa02010
heap2:0xa02030
heap3:0xa02050
heap4:0xa02070
stack1:0x7ffd03d56808
stack2:0x7ffd03d56800
stack3:0x7ffd03d567f8
stack4:0x7ffd03d567f0
[wj@VM-4-3-centos t1109]$
int un_g_val;//未初始化全局数据
int g_val=100;//已初始化全局数据
int main(void)
{
printf("test:%p\n",main);//验证代码区:函数名表示其地址,函数是代码
printf("init:%p\n",&g_val);//验证全局区·已初始化全局区
printf("unint:%p\n",&un_g_val);//验证全局区·未初始化全局区
char*p1=(char*)malloc(16);//动态开辟:开辟的空间在堆上
char*p2=(char*)malloc(16);//动态开辟:开辟的空间在堆上
char*p3=(char*)malloc(16);//动态开辟:开辟的空间在堆上
char*p4=(char*)malloc(16);//动态开辟:开辟的空间在堆上
printf("heap1:%p\n",p1);//验证堆区:P在堆上,*p是指向开辟的堆上地址空间的指针变量,在main函数(栈)中创建,处于栈上。
printf("heap2:%p\n",p2);//验证堆区:P在堆上,*p是指向开辟的堆上地址空间的指针变量,在main函数(栈)中创建,处于栈上。
printf("heap3:%p\n",p3);//验证堆区:P在堆上,*p是指向开辟的堆上地址空间的指针变量,在main函数(栈)中创建,处于栈上。
printf("heap4:%p\n",p4);//验证堆区:P在堆上,*p是指向开辟的堆上地址空间的指针变量,在main函数(栈)中创建,处于栈上。
printf("stack1:%p\n",&p1);
printf("stack2:%p\n",&p2);
printf("stack3:%p\n",&p3);
printf("stack4:%p\n",&p4);
return 0;
}
3)、上述提到的地址空间是内存吗?
回答:这里所说的地址空间不是内存。
延伸:既然这里的地址空间不是内存,那么它是什么?
演示实例一:
相关代码:
#include<stdio.h>
#include<unistd.h>
int g_val=100;
int main(void)
{
pid_t id=fork();
if(id==0)
{
int cnt=0;
while(1)
{
printf("I am child , pid:%d, ppid:%d, g_val:%d, &g_val:%p\n",
getpid(),getppid(),g_val,&g_val);
sleep(1);
cnt++;
if(cnt==5)
{
g_val=200;
printf("child changed g_val : 100->200 success\n");
}
}
}
else
{
while(1)
{
printf("I am parent , pid:%d, ppid:%d, g_val:%d, &g_val:%p\n",
getpid(),getppid(),g_val,&g_val);
sleep(1);
}
}
return 0;
}
运行结果:
[wj@VM-4-3-centos t1109]$ ./proc.out
I am parent , pid:14257, ppid:17613, g_val:100, &g_val:0x60105c
I am child , pid:14258, ppid:14257, g_val:100, &g_val:0x60105c
I am parent , pid:14257, ppid:17613, g_val:100, &g_val:0x60105c
I am child , pid:14258, ppid:14257, g_val:100, &g_val:0x60105c
I am parent , pid:14257, ppid:17613, g_val:100, &g_val:0x60105c
I am child , pid:14258, ppid:14257, g_val:100, &g_val:0x60105c
I am parent , pid:14257, ppid:17613, g_val:100, &g_val:0x60105c
I am child , pid:14258, ppid:14257, g_val:100, &g_val:0x60105c
I am child , pid:14258, ppid:14257, g_val:100, &g_val:0x60105c
I am parent , pid:14257, ppid:17613, g_val:100, &g_val:0x60105c
child changed g_val : 100->200 success
I am child , pid:14258, ppid:14257, g_val:200, &g_val:0x60105c
I am parent , pid:14257, ppid:17613, g_val:100, &g_val:0x60105c
I am child , pid:14258, ppid:14257, g_val:200, &g_val:0x60105c
I am parent , pid:14257, ppid:17613, g_val:100, &g_val:0x60105c
I am child , pid:14258, ppid:14257, g_val:200, &g_val:0x60105c
I am parent , pid:14257, ppid:17613, g_val:100, &g_val:0x60105c
I am child , pid:14258, ppid:14257, g_val:200, &g_val:0x60105c
I am parent , pid:14257, ppid:17613, g_val:100, &g_val:0x60105c
I am child , pid:14258, ppid:14257, g_val:200, &g_val:0x60105c
I am parent , pid:14257, ppid:17613, g_val:100, &g_val:0x60105c
^C
[wj@VM-4-3-centos t1109]$
前提先知:
0、fork之后,代码父子进程共享,那么变量g_val父子进程也要同时共享
观察现象:
1、子进程改变全局变量前:二者g_val值相同,&g_val地址值相同
2、子进程改变全局变量后:二者g_val值不同,&g_val地址值相同
分析:
1、进程具有独立性,子进程修改了g_val值,不会影响父进程的g_val值。符合之前所学内容。
2、同一个地址,同时读取的时候,出现了不同的值。只能说明这里的地址,绝对不可能是物理内存的地址。
延伸: 如果这里的地址不是物理内存的地址,那么&取地址得到的是什么?
回答:虚拟地址(线性地址)
1、几乎所有语言,如果有“地址”的概念,这个地址一定不是物理地址,而是虚拟地址。
1.1.2、地址空间再次综述演示
1)、代码举例
以下为验证代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_unval;
int g_val = 100;
int main(int argc, char *argv[], char *env[])
{
//int a = 10;
//字面常量
//const char *str = "helloworld";
// 10;
// 'a';
printf("code addr: %p\n", main);
printf("init global addr: %p\n", &g_val);
printf("uninit global addr: %p\n", &g_unval);
printf("\n");
int test1 = 10;
static int test2 = 10;
char *heap_mem = (char*)malloc(10);
char *heap_mem1 = (char*)malloc(10);
char *heap_mem2 = (char*)malloc(10);
char *heap_mem3 = (char*)malloc(10);
printf("heap addr: %p\n", heap_mem); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\n", heap_mem1); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\n", heap_mem2); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\n", heap_mem3); //heap_mem(0), &heap_mem(1)
printf("\n");
printf("test1 addr: %p\n", &test1);
printf("test2 stack addr: %p\n", &test2);
printf("\n");
printf("stack addr: %p\n", &heap_mem); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem1); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem2); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem3); //heap_mem(0), &heap_mem(1)
printf("\n");
//printf("read only string addr: %p\n", str);
int i=0;
for( i = 0 ;i < argc; i++)
{
printf("argv[%d]: %p\n", i, argv[i]);
}
for( i = 0; env[i]; i++)
{
printf("env[%d]: %p\n", i, env[i]);
}
return 0;
}
2)、对堆栈地址的说明
如图示:
1、堆区和栈区地址之间差距很大的原因?
中间间隔了共享区。
2、堆区malloc开辟出来的地址相差值?(即我们只开辟了10字节,为什么这里显示的值更多?)
一个问题:free只传入空间起始地址,其怎么知道需要释放多少空间
回答:malloc多申请了一些空间地址,以用来记录当前malloc出来的属性信息。
3)、对static定义的局部变量的说明
如图示:
问题:为什么从操作系统角度理解static
定义局部变量,其作用域在当前函数有效,生命周期却具有全局属性?
回答:被static
修饰的局部变量,其地址空间到了全局数据区。(之前我们只是在语言角度上理解,这里给出了实际验证)
4)、对字面常量的说明
问题:如图示,直接输入字面常量,是否能编译通过?
回答:图示状态能编译通过,因为它们都是字面常量。比如:int a =10
,a
是变量,10
是字面常量,是把字面常量值放入了变量a里。
以下为是字面常量所处地址空间的相关验证:
问题:给字符串常量赋一个指向它的指针,该字符串常量其地址空间在哪?(对字符串常量区的理解)
回答:实际上这些字面常量,是硬编码进代码的。代码区中有一块小区域,是字符常量区,是只读的。
1.1.3、两个补充问题:
1)、用户空间 vs 内核空间
1、32位下,一个进程的地址空间,取值范围是0X 0000 0000~ 0X FFFF FFFF。
2、其中,[0,3GB]为用户空间,[3GB,4GB]为内核空间。
3、地址空间=用户空间+内核空间。
2)、Liunx vs Windows
1、上述的验证在windows下结果不尽相同。默认上述结论在Linux下有效。
1.2、地址空间是什么
1.2.1、阶段认识一:故事引入
阶段认识一:
1.2.2、阶段认识二:虚拟地址和映射机制
阶段认识二:
1、历史上曾有直接访问物理内存的情况,由于内存本身可以随时读写,会存在非法地址的现象。
2、出于安全考虑,引入 虚拟地址、映射机制。虽然最终还是会访问到物理地址上,但映射机制存在检测访问是否非法并拒接映射的能力。
1.2.3、阶段认识三:内核数据结构和相关问题解释
阶段认识三:
1)、虚拟地址空间究竟是什么?
1、问:地址空间内部存在众多区域,如何理解区域划分?
答:区域划分本质上是在一个范围内定义出结构体变量,用start\end
表示其范围。
举例如下:
struct addr_room
{
//代码区
int code_start;
int code_end;
//已初始化全局区
int init_start;
int init_end;
//未初始化全局区
int uninit_start;
int uninit_end;
//堆区
int heap_start;
int heap_end;
//栈区
int stack_start;
int stack_end;
……
//其它属性
}
结论1:地址空间是一种内核数据结构,其里面至少要有各个区域的划分。
需要注意的是,每个区域的划分并不是固定一成不变的,就像栈区向下增长,堆区向上增长,实际上就是改变start、end
指向范围,从而改变区域范围。
2、问:页表,映射机制的表现形式。(该概念后续学习)
结论2: 地址空间和页表(用户级)是每个进程都私有一份。只要保证每个进程的页表映射的是物理内存的不同区域,就能做到进程之间不互相干扰,从而保证进程的独立性。
3、问题: 之前的演示实例一中,为什么地址一样,而值不一样?
地址一样: 父子进程在虚拟地址存放g_val
变量的位置一致(因为子进程是根据父进程拷贝的来)
值一样: 通过页表投射到物理内存时,子进程会出现写时拷贝(后续内容)现象,最终结果是父子进程在物理内存中存储位置不一样,从而保证进程独立性。
总结: 同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址。
相同问题: pid _t ret=fork();
,fork
内部return
执行两次。为什么ret
能保存不同的两个返回值?
扩展:
1、当我们的程序编译形成可执行程序但没有运行的时候(此时还没有被加载到内存中),请问,我们的程序内部有地址吗?
验证如下:可执行程序在编译的时候内部已经有地址了。
2、根据上述需要明白的是,对地址空间,其不仅仅对OS内部适用,对编译器也同样适用。编译器在编译代码时,便是按照地址空间分布分形成各个区域,比如,在Linux下编译程序,就是采用与Linux内核一致的编址方式,对每个变量、每行代码进程编址。因此,程序在编译的时候,其每个字段已经形成了一个虚拟地址。
2)、映射关系的维护是谁来做的?
1.3、为什么要有地址空间
1.3.1、理由一
1)、理由一:
1、凡是非法的访问或者映射,OS都会识别到并终止相关进程(即通过拦截动作有效地保护物理内存)。
int main()
{
char *str="hello world!";
char *s='H'//error;
如上述例子,实际中物理内存是不会对写入数据进行检测或限制。只不过由于地址空间和页表是OS维护的,我们可以在软件层面让OS对其进行监管,当代码在语言层面上出现问题时,是系统层面上进程被OS杀掉。
此处存在两个问题:
1、操作系统如何识别错误?
2、操作系统如何终止错误进程?
1.3.2、理由二
2)、理由二:
问题1、有了地址空间和页表映射的存在,在物理内存中,对于未来的数据,请问,我们是否可以进行任意位置的加载?
回答: 是。页表能通过映射关系一一找到。
结论: 这样一来,物理内存的分配和进程管理就能做到没有关联性。即内存管理模块和进程管理模块可以实现解耦合。
问题2、我们在C、C++语言上malloc、new一块空间时,是在哪里申请的呢?
回答: 在虚拟地址空间。
问题3、如果我们申请了物理空间而不立马使用,是不是对空间的浪费呢?
回答: 是的。
结论: 因为有地址空间的存在,上层申请空间实际上是在地址空间申请的,而真正的物理内存并没有被分配出去。只有当你真正需要对物理地址空间访问的时候,操作系统才会执行内存相关的管理算法,进行内存申请和页表映射关系的构建。这种相当于延迟分配的过程是由操作系统自动完成的,目标是提高内存使用效率从而提高整机效率,而对于用户、对于进程,二者都出于零感知状态,只是自然而然地进行内存访问。
此处涉及一个缺页中断的概念。
1.3.3、理由三
3)、理由三:
1、物理内存在理论上可以任意位置加载,就意味着物理地址中几乎所有数据和代码在其内部是乱序的。但是因为有了页表的存在,它可以将虚拟地址空间和物理地址映射,所以,在进程的视角,内存分布是有序的。
2、操作系统既能够让物理内存延迟分配,那么同样也能够让进程映射到不同的物理内存中,从而实现进程的独立性,让每个进程都认为自己拥有的是4G(32位)空间,并且各个区域都是有序的。站在进程的角度,每个进程都不知道也不需要知道其它进程的存在。
1.3.4、关于挂起的理解
4)、重新理解什么是挂起:
1、加载程序的本质就是创建进程,那么是不是说必须立马将程序内的所有代码数据都加载到内存中并建立映射关系呢?
回答:不需要。(比如,大型游戏120G,OS内存只有8G、16G,它不可能一次性就将其全部加载)
2、基于此,在一些情况下,加载创建进程时,只有其内核结构被创建出来了。这时期的进程状态就称之为新建。
3、基于此,从理论上讲,OS能实现对程序的分批加载(换入),那么同样也能实现对程序的分批换出。那么,当进程短时间内不会再被执行时,OS就可以将相关进程的数据和代码换出,就称之为挂起。
延伸:页表映射的时候,能够映射的不仅仅是内存,磁盘中的位置也可以映射。(那么在挂起时,也可以不进行数据换入换出,在页表处直接记录个磁盘位置,之后换入时找到相关位置即可)。
2、进程控制
2.1、进程创建
2.1.1、fork
1)、请描述一下,fork创建子进程时,操作系统都做了什么?
前提先知:进程=内核数据结构(由操作系统维护)+进程代码和数据(一般由磁盘得来,即C/C++程序加载之后的结果)。
简述性回复:创建子进程,即操作系统中多了一个进程,相应的需要为其带上对应的PCB结构体、对应的虚拟地址空间、页表,并将其代码数据加载到物理内存中构建映射关系。然后将该进程PCB放入到CPU调度队列里,等待进程调度。
2)、fork的最基本使用我们在进程概念章节有所讲述,此处只是简单提及。
3)、问题:使用fork之后父子进程代码共享。是共享fork之后的代码,还是共享所有的代码?
回复:共享所有的代码。
问题:既如此,为什么子进程只执行fork后的代码而不执行整体共享的代码?
回答:
1、我们的代码在汇编之后,会有很多行代码,每行代码被加载到内存后,有其对应的地址。
2、根据CPU对进程的调度方式,进程随时可能会被中断,等它在下一次回来再被执行时,需要从原先执行位置之后接着继续执行(而非又从头开始)
3、因此需要CPU随时记录下当前进程执行的位置,所有CPU内有对应的寄存器数据(一般是EIP,即PC指针,也称程序计数器,用于记录当前执行代码的下一行代码的地址),用来记录当前进程的执行位置。
4、根据之前我们所学,寄存器在CPU内只有一套,但寄存器内可记录多份进程数据(进程上下文的内容),而进程上下文数据也是进程的一部分。
5、因此,在创建子进程时,也要将该数据拷贝给子进程一份。虽然之后父子进程各自调度,各自会修改EIP,但子进程已经认为自己的EIP起始值就是fork之后的代码。
一个小实验:main函数递归。
2.1.2、写时拷贝
1)、关于为什么需要采用写时拷贝技术的问题引入
需要知道的内容:
1、创建子进程时,要给子进程分配对应的内核结构,以保障进程独立性。但事实上,一般情况下我们创建子进程时,没有加载的过程,因此子进程也就没有自己的代码和数据。
2、因此,子进程使用的是父进程的代码和数据。
推进:
1、对于代码,其只读不写,所以父子共享没问题。
2、对于数据,其能够被修改,因此需要分离。
推进:既然父子进程的数据需要分离处理,那么可以怎么分离呢?
方法一:在创建子进程时,就将父子进程的数据直接拷贝分离。
存在问题:为子进程拷贝的这个数据空间,可能后续用不到,或者用到了也只是读取数据而不做修改。
改进:创建子进程时,对于那些不需要立马访问的数据或者只是读取的数据,就不进行拷贝,直接使用父进程的数据。
推进:那么什么样的数据值得拷贝?
回答:将来会被父进程或子进程写入的数据。
问题:
1、一般而言,OS如何提前知道空间内的哪些数据需要被写入?
2、就算提前拷贝了,能保证立马使用这些数据而不造成OS空间浪费吗?
基于上述情况,OS选择写时拷贝技术来将父子进程分离。
方法二:写时拷贝
1、用时再分配,是高效使用内存的一种表现
2、OS无法在代码执行前预知那些空间会被访问。
2.2、进程终止
2.2.1、进程终止时,操作系统做了什么?
1、进程终止时,操作系统要释放相关进程的内核数据结构以及对应的数据和代码。本质上是操作系统在释放系统资源(主要是CPU内存)。
2.2.2、进程终止的常见方式?
1)、进程终止的常见三情形
情形一:代码跑完,结果正确
情形二:代码跑完,结果不正确
情形三:代码没有跑完,程序崩溃(相关重点在信号部分涉及)
关于一、二,我们以下述场景来分析
2)、进程退出码
一个问题:main函数的返回值具有什么意义?return 0的含义是什么?为什么总是返回0?
回答:
1、main函数的返回值是进程退出码。其将数值返回给上一级进程(比如父进程),用来评判该进程执行结果。
2、通常情况下,main函数返回0
是表示success
,非0
表示运行结果不正确
。
3、非零数值有无数个,因此不同的非零数值就可以标识不同的错误原因。这样当程序运行结束结果不正确时,我们可以很方便的定义错误原因。
如何查看最近一次执行完毕的退出码?
相关指令:echo $?
:获取最近一个进程执行完毕的退出码
[wj@VM-4-3-centos t1113]$ make
gcc -o test.out test.c
[wj@VM-4-3-centos t1113]$ ls
Makefile test.c test.out
[wj@VM-4-3-centos t1113]$ ./test.out
hello vim!
[wj@VM-4-3-centos t1113]$ cat test.c //这是Test.c的内容
#include<stdio.h>
int main()
{
printf("hello vim!\n");
return 10; //我们将返回值设置为10
}
[wj@VM-4-3-centos t1113]$ echo $? //执行该条指令可看到最近一次进程(即main函数)的退出码是10.
10
[wj@VM-4-3-centos t1113]$ echo $? //再次执行结果为0是因此这里显示的是上一个echo $?的结果
0
[wj@VM-4-3-centos t1113]$ echo $?
0
[wj@VM-4-3-centos t1113]$
如何获取退出码,将其转换为我们认识的错误信息?
相关函数:strerror
,将系统对应的错误码/退出码转换为字符串描述
相关代码演示及结果
#include<stdio.h>
#include<string.h>
int main()
{
int i=0;
for(i=0;i<150;i++)
{
printf("number%d:%s\n",i,strerror(i));
}
return 0;
}
number134:Unknown error 134
,到134后就没有了。
[wj@VM-4-3-centos t1113]$ ./test.out
number0:Success
number1:Operation not permitted
number2:No such file or directory
number3:No such process
number4:Interrupted system call
number5:Input/output error
number6:No such device or address
number7:Argument list too long
number8:Exec format error
number9:Bad file descriptor
number10:No child processes
number11:Resource temporarily unavailable
number12:Cannot allocate memory
number13:Permission denied
number14:Bad address
number15:Block device required
number16:Device or resource busy
number17:File exists
number18:Invalid cross-device link
number19:No such device
number20:Not a directory
number21:Is a directory
number22:Invalid argument
number23:Too many open files in system
number24:Too many open files
number25:Inappropriate ioctl for device
number26:Text file busy
number27:File too large
number28:No space left on device
number29:Illegal seek
number30:Read-only file system
number31:Too many links
number32:Broken pipe
number33:Numerical argument out of domain
number34:Numerical result out of range
number35:Resource deadlock avoided
number36:File name too long
number37:No locks available
number38:Function not implemented
number39:Directory not empty
number40:Too many levels of symbolic links
number41:Unknown error 41
number42:No message of desired type
number43:Identifier removed
number44:Channel number out of range
number45:Level 2 not synchronized
number46:Level 3 halted
number47:Level 3 reset
number48:Link number out of range
number49:Protocol driver not attached
number50:No CSI structure available
number51:Level 2 halted
number52:Invalid exchange
number53:Invalid request descriptor
number54:Exchange full
number55:No anode
number56:Invalid request code
number57:Invalid slot
number58:Unknown error 58
number59:Bad font file format
number60:Device not a stream
number61:No data available
number62:Timer expired
number63:Out of streams resources
number64:Machine is not on the network
number65:Package not installed
number66:Object is remote
number67:Link has been severed
number68:Advertise error
number69:Srmount error
number70:Communication error on send
number71:Protocol error
number72:Multihop attempted
number73:RFS specific error
number74:Bad message
number75:Value too large for defined data type
number76:Name not unique on network
number77:File descriptor in bad state
number78:Remote address changed
number79:Can not access a needed shared library
number80:Accessing a corrupted shared library
number81:.lib section in a.out corrupted
number82:Attempting to link in too many shared libraries
number83:Cannot exec a shared library directly
number84:Invalid or incomplete multibyte or wide character
number85:Interrupted system call should be restarted
number86:Streams pipe error
number87:Too many users
number88:Socket operation on non-socket
number89:Destination address required
number90:Message too long
number91:Protocol wrong type for socket
number92:Protocol not available
number93:Protocol not supported
number94:Socket type not supported
number95:Operation not supported
number96:Protocol family not supported
number97:Address family not supported by protocol
number98:Address already in use
number99:Cannot assign requested address
number100:Network is down
number101:Network is unreachable
number102:Network dropped connection on reset
number103:Software caused connection abort
number104:Connection reset by peer
number105:No buffer space available
number106:Transport endpoint is already connected
number107:Transport endpoint is not connected
number108:Cannot send after transport endpoint shutdown
number109:Too many references: cannot splice
number110:Connection timed out
number111:Connection refused
number112:Host is down
number113:No route to host
number114:Operation already in progress
number115:Operation now in progress
number116:Stale file handle
number117:Structure needs cleaning
number118:Not a XENIX named type file
number119:No XENIX semaphores available
number120:Is a named type file
number121:Remote I/O error
number122:Disk quota exceeded
number123:No medium found
number124:Wrong medium type
number125:Operation canceled
number126:Required key not available
number127:Key has expired
number128:Key has been revoked
number129:Key was rejected by service
number130:Owner died
number131:State not recoverable
number132:Operation not possible due to RF-kill
number133:Memory page has hardware error
number134:Unknown error 134
number135:Unknown error 135
number136:Unknown error 136
number137:Unknown error 137
number138:Unknown error 138
number139:Unknown error 139
number140:Unknown error 140
number141:Unknown error 141
number142:Unknown error 142
number143:Unknown error 143
number144:Unknown error 144
number145:Unknown error 145
number146:Unknown error 146
number147:Unknown error 147
number148:Unknown error 148
number149:Unknown error 149
[wj@VM-4-3-centos t1113]$
我们可以自己使用上述这些退出码,也可以自己设计一套退出方案。
[wj@VM-4-3-centos t1113]$ ls
Makefile test.c test.out
[wj@VM-4-3-centos t1113]$ ls aaaa.c //让ls执行一条不存在的命令
ls: cannot access aaaa.c: No such file or directory //ls的返回结果,根据上述显示可知其是错误信息中的一条
[wj@VM-4-3-centos t1113]$ echo $? //查看退出码验证确实如此。
2
[wj@VM-4-3-centos t1113]$
程序崩溃与退出码
1、程序崩溃时,退出码是无意义的。因为一般情况下退出码对应的return语句没有被执行。
#include<stdio.h>
#include<string.h>
int main()
{
printf("hello vim1!\n");
printf("hello vim2!\n");
printf("hello vim3!\n");
int *p=NULL;
*p=223344;
printf("hello vim4!\n");
printf("hello vim5!\n");
printf("hello vim6!\n");
printf("hello vim7\n");
return 0;
}
[wj@VM-4-3-centos t1113]$ ./test.out
hello vim1!
hello vim2!
hello vim3!
Segmentation fault //执行结果:可看到第三行打印后就报错。
[wj@VM-4-3-centos t1113]$
2.2.3、如何正确的用代码终止一个进程?
方法一:main函数中的return语句能够终止进程,return + 退出码即可终止进程。
#include<stdio.h>
#include<string.h>
int add(int sum)
{
int i=0;
int result=0;
for(i=0;i<sum;i++)
{
result+=i;
}
return result;
}
int main()
{
printf("hello vim1!\n");
printf("hello vim2!\n");
printf("hello vim3!\n");
int sum=add(100);
return 11;
//int *p=NULL;
//*p=223344;
printf("hello vim4!\n");
printf("hello vim5!\n");
printf("hello vim6!\n");
printf("hello vim7\n");
return 10;
}
[wj@VM-4-3-centos t1113]$ make
gcc -o test.out test.c
[wj@VM-4-3-centos t1113]$ ./test.out
hello vim1!
hello vim2!
hello vim3!
[wj@VM-4-3-centos t1113]$ echo $? //可看到退出码为11
11
[wj@VM-4-3-centos t1113]$
需要注意的是,此处的return语句需要是main函数中的return语句才起效。比如上述代码中的sum返回了return,但其不是进程终止而是函数返回。
方法二:exit在代码任何地方调用,都能终止进程。
演示实例一:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int add(int sum)
{
int i=0;
int result=0;
for(i=0;i<sum;i++)
{
result+=i;
}
return result;
}
int main()
{
printf("hello vim1!\n");
printf("hello vim2!\n");
printf("hello vim3!\n");
int sum=add(100);
exit(111); //在main函数中调用exit
printf("hello vim4!\n");
printf("hello vim5!\n");
printf("hello vim6!\n");
printf("hello vim7\n");
return 10;
}
[wj@VM-4-3-centos t1113]$ ./test.out
hello vim1!
hello vim2!
hello vim3!
[wj@VM-4-3-centos t1113]$ echo $?
111
[wj@VM-4-3-centos t1113]$
演示实例二:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int add(int sum)
{
int i=0;
int result=0;
for(i=0;i<sum;i++)
{
result+=i;
}
exit(222);
// return result;
}
int main()
{
printf("hello vim1!\n");
printf("hello vim2!\n");
printf("hello vim3!\n");
int sum=add(100);//在add函数内置exit
exit(111);
printf("hello vim4!\n");
printf("hello vim5!\n");
printf("hello vim6!\n");
printf("hello vim7\n");
return 10;
}
[wj@VM-4-3-centos t1113]$ ./test.out
hello vim1!
hello vim2!
hello vim3!
[wj@VM-4-3-centos t1113]$ echo $?
222
[wj@VM-4-3-centos t1113]$
方法三:_exit系统调用
演示实例一:常规使用方法
仍旧用上述代码进行实验,只是把exit换为了_exit,可以看到情况相同。
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
int add(int sum)
{
int i=0;
int result=0;
for(i=0;i<sum;i++)
{
result+=i;
}
_exit(222);
//exit(222);
// return result;
}
int main()
{
printf("hello vim1!\n");
printf("hello vim2!\n");
printf("hello vim3!\n");
int sum=add(100);
_exit(111);
//exit(111);
//return 11;
printf("hello vim4!\n");
printf("hello vim5!\n");
printf("hello vim6!\n");
printf("hello vim7\n");
return 10;
}
演示实例二:_exit与exit的区别点
[wj@VM-4-3-centos t1113]$ cat test.c
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("you can see me.\n");
sleep(3);
return 11;
}
[wj@VM-4-3-centos t1113]$ ./test.out //在之前学进度条时我们谈论过缓冲区和换行符的问题
you can see me. //此时演示结果为:先打印字段再睡眠三秒结束。
[wj@VM-4-3-centos t1113]$ echo $?
11
[wj@VM-4-3-centos t1113]$
将return换为exit函数。
[wj@VM-4-3-centos t1113]$ cat test.c
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
printf("you can see me.");//去掉\n
sleep(3);
exit(11);
//return 11;
}
[wj@VM-4-3-centos t1113]$ ./test.out
you can see me.[wj@VM-4-3-centos t1113]$ ls
Makefile test01.c test.c test.out
[wj@VM-4-3-centos t1113]$ ./test.out
you can see me.[wj@VM-4-3-centos t1113]$ echo $? //现象:进程睡眠三秒,然后打印
11
将exit换为_exit函数。
[wj@VM-4-3-centos t1113]$ cat test.c
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
printf("you can see me.");
sleep(3);
_exit(11);
//return 11;
}
[wj@VM-4-3-centos t1113]$ make
gcc -o test.out test.c
[wj@VM-4-3-centos t1113]$ ./test.out //现象:程序运行结束,但输出结果没有被刷新出来。
[wj@VM-4-3-centos t1113]$ echo $?
11
[wj@VM-4-3-centos t1113]$ ./test.out
[wj@VM-4-3-centos t1113]$ echo $?
11
[wj@VM-4-3-centos t1113]$
后续:地址空间与进程控制(二)