处理进程的错误信息
系统维护着一个全局变量int errno,我们把这个变量的值称为错误编号,通过错误编号可以找到编号对应的详细错误描述。当函数异常退出的时候,系统会设置errno的值,系统只保留最近一次函数异常时对errno的赋值。大部分的系统调用和库函数异常退出的时候都会对errno进行赋值。例如操作文件的函数fopen函数,在fopen函数的返回值中有如下描述:
RETURN VALUE
Upon successful completion fopen(), fdopen() and freopen() return a
FILE pointer. Otherwise, NULL is returned and errno is set to indi‐
cate the error.
如果打开失败,fopen函数返回一个NULL地址,并且将errno进行设置。
只要在系统调用函数和库函数有看到如上的描述,就可以通过errno获取函数调用失败时的错误编码。
拿到错误编码之后,系统提供了两个函数来获取这个错误编码对应的详细错误描述
char *strerror(int errnum);
#include <string.h>
char *strerror(int errnum);
功能:return string describing error number
参数:int errnum
返回值:字符类型的指针
例子如下:
#include<stdio.h>
#include<errno.h>
#include<string.h>
int main(int argc,char **argv){
FILE *fp = fopen(*(argv + 1),"r");
if(fp == NULL){
printf("error is :%s\n",strerror(errno));
return -1;
}
printf("%s open success...\n",*(argv + 1));
fclose(fp);
fp = NULL;
return 0;
}
\----------------------------------------------------
linxin@ubuntu:~/UC/day04$ Demo
error is :Bad address
linxin@ubuntu:~/UC/day04$ Demo hello
error is :No such file or directory
上述函数用于打开一个文件,如果打开失败,就返回具体的错误描述。
可以看到,当命令行中,只有Demo的时候,错误信息是“Bad address”,
当命令行后,跟上一个不存在的文件的时候,错误信息是“No such file or directory”
一个函数调用失败的不同情况,有它不同的错误原因。
void perror(const char *s);
#include <stdio.h>
void perror(const char *s);
#include <errno.h>
功能:print a system error message
参数:一个字符串
详细描述:这个函数会先打印传给它的字符串,然后再字符串的后面跟上冒号(:)和一个空格(:),然后打印当前errno对应的详细错误描述,如果找不到errno对应的详细错误信息,将返回字符串:"Unknown error nnn"
例子如下:
#include<stdio.h>
#include<errno.h>
#include<string.h>
int main(int argc,char **argv){
FILE *fp = fopen(*(argv + 1),"r");
if(fp == NULL){
perror("fopen's error is");
return -1;
}
printf("%s open success...\n",*(argv + 1));
fclose(fp);
fp = NULL;
return 0;
}
\---------------------------------------------------
linxin@ubuntu:~/UC/day04$ Demo
fopen's error is: Bad address
linxin@ubuntu:~/UC/day04$ Demo hello
fopen's error is: No such file or directory
上面两个函数的调用,都需要包含 errno.h 的头文件,可以在函数的描述中查看调用该函数需要包含的头文件。
内存管理
在计算机中,内存不仅仅指代内存条,还有主板上的ROM和其他芯片中的只读内存。
这里,了解一下CPU是通过什么样的方式来访问内存的,在这里讨论的是32位系统。
如图所示:
CPU不是直接访问物理内存,而是通过一块虚拟地址来间接访问物理内存。操作系统维护着一张页表,这张页表中存放着虚拟地址和物理地址的映射关系,当CPU访问虚拟地址时,操作系统会找到这块虚拟地址所映射的物理地址,让CPU间接访问。虚拟地址和物理地址映射的单位是页,一页代表4k的内存。
这种间接访问的方式,并不要求物理内存一定要达到4G。假设物理内存只有1G,虚拟地址中,0和1G的内存地址对应同一块物理地址,等到虚拟地址0使用完这块物理内存之后,再由1G地址使用。由于存在时间差,所以就不会产生坏的影响。
如果CPU访问了一个虚拟地址,而这个虚拟地址没有映射到物理地址,那么就会报错,段错误;
同样,如果当前执行的程序并不具备访问的这个虚拟地址的权限,强行访问也会造成,段错误。
一个程序链接,和加载到内存时的情况
上述内容,已经知道CPU是如何访问一块内存的。现在来讨论一下,一个进程在内存中的情况。
在哈弗体系的系统中,为了减小CPU的负担,进程中的指令和数据是分开存放到内存中的。
现在暂时的将一个进程在内存中的存放分为,代码段,数据段,栈段。
这三段的内容,需要从程序从源文件编译成目标文件时说起。
三段的内容,可以在汇编文件中看见,如下:
.file "Demo.c"
.section .rodata
.LC0:
.string "r"
.LC1:
.string "fopen's error is"
.LC2:
.string "%s open success...\n"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $32, %rsp
movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
movq -32(%rbp), %rax
addq $8, %rax
movq (%rax), %rax
leaq .LC0(%rip), %rsi
movq %rax, %rdi
call fopen@PLT
movq %rax, -8(%rbp)
cmpq $0, -8(%rbp)
jne .L2
leaq .LC1(%rip), %rdi
call perror@PLT
movl $-1, %eax
jmp .L3
.L2:
movq -32(%rbp), %rax
addq $8, %rax
movq (%rax), %rax
movq %rax, %rsi
leaq .LC2(%rip), %rdi
movl $0, %eax
call printf@PLT
movq -8(%rbp), %rax
movq %rax, %rdi
call fclose@PLT
movq $0, -8(%rbp)
movl $0, %eax
.L3:
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 7.2.0-8ubuntu3.2) 7.2.0"
.section .note.GNU-stack,"",@progbits
上述是一个汇编文件的内容,
文件的第二行中的 .section .rodata 表示只读数据部分;
文件的第九行中的 .text 表示代码块部分;
文件的最后一行的 .section .note.GNU-stack,”“,@progbits 表示栈部分
当一个程序的所有源文件都编译成目标文件之后,进入链接阶段。
链接阶段会把每个目标文件中的数据部分都整理出来放在可执行文件的数据部分,
把每个目标文件中的代码块部分整理出来放在可执行文件的代码块部分,
把每个目标文件中的栈部分整理出来放在可执行文件的栈部分。
当系统把可执行文件从硬盘加载到内存中的时候,会分开存放这三部分。然后通过三个不同的映射,分别映射到虚拟地址空间的代码段,数据段,栈段。
代码段:可读,不可写,可执行
数据段:可读,可写,不可执行
栈段:可读,可写,不可执行
当进程在执行一个函数(fun1)的过程中,调用了另外一个函数(fun2),系统需要为fun1函数保留离开之前的所有变量状态。系统将fun1的状态压栈,保存在栈段顶部,然后运行fun2
每个进程都有一份映射图,可以看到当前进程各个段在虚拟地址中的范围。这份映射图保存在/proc/pid/maps中。所以如果想要查看这份映射图,就必须知道进程的pid。
系统提供了一个查看当前进程的pid的函数,如下:
pid_t getpid(void);
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
功能:返回调用该函数的进程的pid
返回值:一个整型的返回值,代表了进程的pid
结合以上知识点,我们可以来深究一下变量生命周期的原理。代码如下:
#include<stdio.h>
#include <sys/types.h>
#include <unistd.h>
char numfive = 0;
void run(int num){
static char numtwo = 0;
const char numfore = 0;
printf("static char numtwo:%p\n",&numtwo);
printf("const char numfore:%p\n",&numfore);
printf("char int numfive:%p\n",&numfive);
printf("num:%p\n",&num);
getchar();
}
int main(void){
char *numone = "hello";
printf("pid:%d\n",getpid());
printf("numone:%p\n",numone);
printf("&numone:%p\n",&numone);
run(10);
getchar();
return 0;
}
上述代码,在main函数中,定义了一个局部变量,char类型的指针,指向一个字符串常量。
在run函数中,定义了一个形式参数,一个静态局部变量,一个局部常量
一个全局变量
现在,我们来看一下,当这个程序运行之后,这些变量在什么位置:
5abeace9000-55abeacea000 r-xp 00000000 08:01 1048744 /home/linxin/UC/day04/test1
55abeaee9000-55abeaeea000 r--p 00000000 08:01 1048744 /home/linxin/UC/day04/test1
55abeaeea000-55abeaeeb000 rw-p 00001000 08:01 1048744 /home/linxin/UC/day04/test1
55abebaed000-55abebb0e000 rw-p 00000000 00:00 0 [heap]
7fdb285db000-7fdb287b1000 r-xp 00000000 08:01 1572948 /lib/x86_64-linux-gnu/libc-2.26.so
7fdb287b1000-7fdb289b1000 ---p 001d6000 08:01 1572948 /lib/x86_64-linux-gnu/libc-2.26.so
7fdb289b1000-7fdb289b5000 r--p 001d6000 08:01 1572948 /lib/x86_64-linux-gnu/libc-2.26.so
7fdb289b5000-7fdb289b7000 rw-p 001da000 08:01 1572948 /lib/x86_64-linux-gnu/libc-2.26.so
7fdb289b7000-7fdb289bb000 rw-p 00000000 00:00 0
7fdb289bb000-7fdb289e2000 r-xp 00000000 08:01 1572880 /lib/x86_64-linux-gnu/ld-2.26.so
7fdb28bc0000-7fdb28bc2000 rw-p 00000000 00:00 0
7fdb28be2000-7fdb28be3000 r--p 00027000 08:01 1572880 /lib/x86_64-linux-gnu/ld-2.26.so
7fdb28be3000-7fdb28be4000 rw-p 00028000 08:01 1572880 /lib/x86_64-linux-gnu/ld-2.26.so
7fdb28be4000-7fdb28be5000 rw-p 00000000 00:00 0
7ffed368a000-7ffed36ab000 rw-p 00000000 00:00 0 [stack]
7ffed36cb000-7ffed36ce000 r--p 00000000 00:00 0 [vvar]
7ffed36ce000-7ffed36d0000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
/----------------------------------------------------------------
linxin@ubuntu:~/UC/day04$ test1
pid:6366
numone:0x55abeace994f
&numone:0x7ffed36a80a0
static char numtwo:0x55abeaeea012
const char numfore:0x7ffed36a8087
char int numfive:0x55abeaeea011
num:0x7ffed36a807c
通过对比地址,可以发现:
在栈段中包含:自动局部变量、形式参数
在数据段中包含:静态局部变量、全局变量
在代码段中包含:字符常量
实际上自动局部变量和形式参数是在活动函数的栈帧中,当活动的函数出栈之后,系统不再保留原本属于它的栈帧,而是把这片栈帧给到下一个活动的函数,那么原本栈帧中的参数也就会破坏。这就是自动局部变量和形式参数生命周期的本质。
而数据段和代码段,都是随着进程的结束才会结束。所以存放在这两个区域的变量的生命周期也就和进程执行时间一致。