该系列文章总纲链接:专题分纲目录 LinuxC 系统编程
本章节思维导图如下所示(思维导图会持续迭代):
第一层:
第二层:
1 进程的启动和退出
1.1 流程
程序启动 -> 程序加载 & 地址分配 -> 程序退出
@1 程序启动:对于二进制文件:
- 如果文件在usr/bin或者/bin文件夹下等在PATH环境变量中已经指定的地址中,则直接输入二进制文件的名字即可
- 若不在PATH环境变量下,则用 程序路径 程序名(例如./hello)的方式也可以。
- 若不在PATH环境变量下却想直接输入二进制名就能执行文件则将程序路径添加到PATH环境变量里面,将二进制文件移动到PATH环境变量的目录中。
@2 程序加载,地址分配:
加载的简单过程如下:
- 从目标文件中读取足够的头部信息,找出需要多少地址空间。
- 分配地址空间,如果目标代码的格式具有独立的段,那就将地址空间按独立的段来划分。
- 将程序读入地址空间的段中。
- 将程序末尾的bss段空间填充为0(如果虚拟内存系统部自动这么做的话)。
- 创建一个堆栈(若体系结构需要的话)。
- 设置运行信息,比如程序参数、环境变量等。
- 开始运行程序,从_start入口,找到main,开始顺序执行程序。
@3 程序退出:
一般退出有3种方式:
- 进程自愿退出。这体现在exit函数和return函数上。退出时,需要回收进程所分配的资源(比如地址空间、文件描述符等),操作系统会对每一项资源进行善后处理。
- 进程收到一个信号退出。这种情况很常见,往往是父进程对其子进程的终止操作。这一操作实际上是父进程向子进程发送了一个终止信号,子进程接收到信号后也会资自愿退出。
- 进程执行了一个导致异常的操作后退出。上面两种情况都是在程序预期之下退出的,而异常的操作是在程序没有准备的情况下退出的。这时候操作系统对其资源进行回收,但是可能不会对这些资源进行善后处理。异常实际上是一种向进程发送了一个特殊的信号,但是发送信号的不是某一个进程而是操作系统本身。
1.2 进程终止处理函数
linux环境下允许在进程退出的时候调用一些用户自定义的函数,这些函数称为终止处理函数。linux规定最多可以设置32个这样的进程终止处理函数。linux下使用atexit函数设置进程终止处理函数。atexit函数的原型:
#include <stdlib.h>
int atexit(void (*function)(void));
详细见linux函数参考手册。注意:
- 函数执行成功返回0,失败返回非零值。(注意,atexit函数执行失败时不是返回-1)
- 进程终止处理函数的调用顺序与设置的时候相反。(后调用的先结束,类似于栈的结构)
- 实际上,进程终止函数是在进程结束的时候进行的一些辅助性的操作。
2 linux进程内存管理
2.1 大端与小端
一般的PC机采用的是小端的结构,而server一般采用大端结构。这种数据存储的差别并不是由操作系统造成的,大端与小端体现在CPU的体系结构上。一般对其进行编程的时候要先判断是否是大端还是小端,之后对其进行操作。
小端:高地址存高位,低地址存低位。大端:与小端相反。
2.2 代码段、数据段与bss段
@1 代码段:一般是不允许进行写操作的,属性是只读。一个程序多数情况下不需要更改代码段,只有一种情况除外,那就是升级程序,对于server而言,要在不停止程序的情况下完成代码段部分内容的更换。以前一般就是对代码段进行写的操作,直接更换,但是这样风险也很大。现在一般用共享库的方式来解决这个问题。
@2 数据段:
- 初始化数据段(.data):包含程序中明确给定初值的全局变量和静态变量。
- 块存储段(.bss):存储在这个段中的数据通常是没有明确给定初值的全局变量和静态变量。
@3 bss段中的内容并不作为程序文件中的一部分,也就是不包含在二进制文件中,而是被保存在外存上,系统仅仅是在内存中标记了bss段的一些信息(初始化变量大小、属性等);以便于运行程序的时候能够找到bss段中的内容。如果全局变量/静态变量本身有给定的值,而这个值是0/NULL的时候,编译器会将其内容写到bss段,而不是data段中。
2.3 栈与堆
自动变量有3种存储方式:
- bss段:静态局部变量
- 寄存器里:寄存器变量
- 栈:一般自动变量
这里面在编程的时候最常见的错误就是将一个指向局部变量的指针作为函数的返回值返回。由于指针指向的内容还在栈帧上,函数只是将其地址返回。因此如果栈帧被其他函数覆盖,返回的指针指向的内存区域的值也就失效了。
堆空间一般是存储用户申请的内存空间,堆上的操作往往是malloc。栈与堆的位置往往是相对的,但是具体的分配要看处理器的存储结构,与大端小端的差别是类似的。
2.4 常量存储
对于一个简单的常量来说,它是存储在代码段里,因为简单变量的长度是固定的。这样可以加快取指令的速度,还可以提高程序的效率。但那是对于字符串这种复杂的常量而言,其长度不定,如果将字符串存储在代码段中,会导致代码段很大,同时不利于处理器进行代码读入缓冲处理,大大影响程序的执行效率。所以最后单独弄出一个段来存储字符串。
2.5 动态内存管理
系统使用mem_control_block结构管理所有已经分配的内存块,结构如下:
struct mem_control_block{
int is_available; //该块是否可用
int size; //块的大小
}
通过这个结构可以简单的实现malloc函数,整个流程如下:
malloc函数首先将用户要分配的字节数加上一个“内存控制块”的大小,得出实际需要分配的字节数。
- 之后顺序遍历堆中所有的内存块,如果该块可用且大于实际需要的字节数,则将该内存块的首地址返回并将该块设置为可用,否则尝试下一个内存块。
- 如果所有的内存块都不满足条件,则调用sbrk函数(如果sbrk函数失败,则系统中没有可用的内存了,malloc函数返回NULL),通过操作系统分配一块内存。malloc函数将这块内存拓展在堆内,相当于堆增长了。
- 跳过该块内存的“内存控制结构”,将最后一块内存的末地址重新设置。
对于free函数的一些说明: free函数的主要工作就是将内存控制块设置为可用。当下一次调用malloc函数的时候就可以将该内存块作为可分配块进行分配了。因此在调用free函数之后,该内存块中的内容不会立刻消失,但那是这时此内容已经不受操作系统的保护,因此有效的时间也是随机的。
3 shell环境
命令行参数和环境变量这两个信息都是从父进程获得的,其获取方式也不同。
命令行参数作为main函数的参数被传送到新进程中,而环境变量是作为一种全局变量被新进程所使用的。
3.1 命令行参数与应用
argc:命令行参数的个数;
argv:指向参数的各个指针所构成的数组;
这里的argv[0]表示可执行程序的整个路径名,并不只是可执行程序的文件名。(要想通过路径名得到文件名需要进行相应的字符处理);argv[argc]一定是NULL。
3.2 环境变量
每个程序都会有一个环境变量表,和命令行参数一样,环境变量表也是一个指针数组。包含相应的头文件,在程序中写入extern char **environ;通过读environ[i]一直循环直到environ[i] = NULL就可以得到环境变量表,即每个环境变量的值。
注意:在本进程中修改环境变量没有意义,因为不会影响到其他的进程。
环境变量的设置、获取、删除函数原型如下:
#include <stdlib.h>
char *getenv(const char *name);//获取环境变量,成功则返回环境变量的值,失败则返回NULL。
int put(char* str); //将 name==value的字符串放进环境表,如果原来有值则覆盖。
int setenv(const char *name, const char *value, int overwrite);//设置环境变量,这里第3个参数rewrite的值为0则:不修改原来的值;非0值则:修改原来的值。
int unsetenv(const char *name);//删除一个环境变量的值,成功返回0,失败返回-1。
int clearenv(); //此函数会将整个environ这个指针置为NULL,成功返回0,失败返回-1。
详细见linux函数参考手册。以上对这些环境变量的操作的函数仅对其本身的进程和子进程有影响,对于父进程没有影响。
3.3 获取进程结束状态
$?是linux shell中的一个内置变量,其中保存的是最近一次运行程序的返回值。有3中情况:
- 程序中的main函数运行结束,$?中保存main函数的返回值。
- 程序运行中调用exit函数结束运行,$?保存的是exit函数的参数。
- 程序异常退出,$?中保存异常出错的错误号。
注意:
- 如果程序运行出错,那么$?内置变量中的值是1。所以在编写代码的时候,如果代码没有问题,则不要返回1(exit(1)或者return(1))。以免引起不必要的混乱。
- 如果main函数没有返回一个指定的值,那么$?中的值不是随机的,切记!
- 由于linux shell中$?中内置的变量的值实际上是进程结束后eax寄存器的值(仅在X86体系结构下),所以看到的是linux系统在此结构体系中使用eax保存每个函数的返回值。这个值针对不同的系统是不同的。
3.4 使用errno调试程序
调试一个程序的方法往往有以下几种:
- 使用调试器
- 在程序中直接使用输出函数输出调试信息
- 查看标准出错文件
- 程序异常时所写的日志
在linux下执行系统调用的时候会出现一些错误,仅仅通过检查这些系统调用的返回值是不够的,开发者往往需要更加详细的信息。C语言提供了一个全局变量errno,使用时加上头文件<errno.h>,这个全局变量很好地弥补了返回值信息不足的缺点。errno为0表示没有错误,如果出错则输出错误号。使用的时候一定要先清0,因为是全局变量。
常见errno的定义及解读如下表所示:
C Name |
Value |
Description |
含义 |
Success |
0 |
Success |
成功 |
EPERM |
1 |
Operation not permitted |
操作不允许 |
ENOENT |
2 |
No such file or directory |
没有这样的文件或目录 |
ESRCH |
3 |
No such process |
没有这样的过程 |
EINTR |
4 |
Interrupted system call |
系统调用被中断 |
EIO |
5 |
I/O error |
I/O错误 |
ENXIO |
6 |
No such device or address |
没有这样的设备或地址 |
E2BIG |
7 |
Arg list too long |
参数列表太长 |
ENOEXEC |
8 |
Exec format error |
执行格式错误 |
EBADF |
9 |
Bad file number |
坏的文件描述符 |
ECHILD |
10 |
No child processes |
没有子进程 |
EAGAIN |
11 |
Try again |
资源暂时不可用 |
ENOMEM |
12 |
Out of memory |
内存溢出 |
EACCES |
13 |
Permission denied |
拒绝许可 |
EFAULT |
14 |
Bad address |
错误的地址 |
ENOTBLK |
15 |
Block device required |
块设备请求 |
EBUSY |
16 |
Device or resource busy |
设备或资源忙 |
EEXIST |
17 |
File exists |
文件存在 |
EXDEV |
18 |
Cross-device link |
无效的交叉链接 |
ENODEV |
19 |
No such device |
设备不存在 |
ENOTDIR |
20 |
Not a directory |
不是一个目录 |
EISDIR |
21 |
Is a directory |
是一个目录 |
EINVAL |
22 |
Invalid argument |
无效的参数 |
ENFILE* |
23 |
File table overflow |
打开太多的文件系统 |
EMFILE |
24 |
Too many open files |
打开的文件过多 |
ENOTTY |
25 |
Not a tty device |
不是tty设备 |
ETXTBSY |
26 |
Text file busy |
文本文件忙 |
EFBIG |
27 |
File too large |
文件太大 |
ENOSPC |
28 |
No space left on device |
设备上没有空间 |
ESPIPE |
29 |
Illegal seek |
非法移位 |
EROFS |
30 |
Read-only file system |
只读文件系统 |
EMLINK |
31 |
Too many links |
太多的链接 |
EPIPE |
32 |
Broken pipe |
管道破裂 |
EDOM |
33 |
Math argument out of domain |
数值结果超出范围 |
ERANGE |
34 |
Math result not representable |
数值结果不具代表性 |
EDEADLK |
35 |
Resource deadlock would occur |
资源死锁错误 |
ENAMETOOLONG |
36 |
Filename too long |
文件名太长 |
ENOLCK |
37 |
No record locks available |
没有可用锁 |
ENOSYS |
38 |
Function not implemented |
功能没有实现 |
ENOTEMPTY |
39 |
Directory not empty |
目录不空 |
ELOOP |
40 |
Too many symbolic links encountered |
符号链接层次太多 |
EWOULDBLOCK |
41 |
Same as EAGAIN |
和EAGAIN一样 |
ENOMSG |
42 |
No message of desired type |
没有期望类型的消息 |
EIDRM |
43 |
Identifier removed |
标识符删除 |
ECHRNG |
44 |
Channel number out of range |
频道数目超出范围 |
EL2NSYNC |
45 |
Level 2 not synchronized |
2级不同步 |
EL3HLT |
46 |
Level 3 halted |
3级中断 |
EL3RST |
47 |
Level 3 reset |
3级复位 |
ELNRNG |
48 |
Link number out of range |
链接数超出范围 |
EUNATCH |
49 |
Protocol driver not attached |
协议驱动程序没有连接 |
ENOCSI |
50 |
No CSI structure available |
没有可用CSI结构 |
EL2HLT |
51 |
Level 2 halted |
2级中断 |
EBADE |
52 |
Invalid exchange |
无效的交换 |
EBADR |
53 |
Invalid request descriptor |
请求描述符无效 |
EXFULL |
54 |
Exchange full |
交换全 |
ENOANO |
55 |
No anode |
没有阳极 |
EBADRQC |
56 |
Invalid request code |
无效的请求代码 |
EBADSLT |
57 |
Invalid slot |
无效的槽 |
EDEADLOCK |
58 |
Same as EDEADLK |
和EDEADLK一样 |
EBFONT |
59 |
Bad font file format |
错误的字体文件格式 |
ENOSTR |
60 |
Device not a stream |
设备不是字符流 |
ENODATA |
61 |
No data available |
无可用数据 |
ETIME |
62 |
Timer expired |
计时器过期 |
ENOSR |
63 |
Out of streams resources |
流资源溢出 |
ENONET |
64 |
Machine is not on the network |
机器不上网 |
ENOPKG |
65 |
Package not installed |
没有安装软件包 |
EREMOTE |
66 |
Object is remote |
对象是远程的 |
ENOLINK |
67 |
Link has been severed |
联系被切断 |
EADV |
68 |
Advertise error |
广告的错误 |
ESRMNT |
69 |
Srmount error |
srmount错误 |
ECOMM |
70 |
Communication error on send |
发送时的通讯错误 |
EPROTO |
71 |
Protocol error |
协议错误 |
EMULTIHOP |
72 |
Multihop attempted |
多跳尝试 |
EDOTDOT |
73 |
RFS specific error |
RFS特定的错误 |
EBADMSG |
74 |
Not a data message |
非数据消息 |
EOVERFLOW |
75 |
Value too large for defined data type |
值太大,对于定义数据类型 |
ENOTUNIQ |
76 |
Name not unique on network |
名不是唯一的网络 |
EBADFD |
77 |
File descriptor in bad state |
文件描述符在坏状态 |
EREMCHG |
78 |
Remote address changed |
远程地址改变了 |
ELIBACC |
79 |
Cannot access a needed shared library |
无法访问必要的共享库 |
ELIBBAD |
80 |
Accessing a corrupted shared library |
访问损坏的共享库 |
ELIBSCN |
81 |
A .lib section in an .out is corrupted |
库段. out损坏 |
ELIBMAX |
82 |
Linking in too many shared libraries |
试图链接太多的共享库 |
ELIBEXEC |
83 |
Cannot exec a shared library directly |
不能直接执行一个共享库 |
EILSEQ |
84 |
Illegal byte sequence |
无效的或不完整的多字节或宽字符 |
ERESTART |
85 |
Interrupted system call should be restarted |
应该重新启动中断的系统调用 |
ESTRPIPE |
86 |
Streams pipe error |
流管错误 |
EUSERS |
87 |
Too many users |
用户太多 |
ENOTSOCK |
88 |
Socket operation on non-socket |
套接字操作在非套接字上 |
EDESTADDRREQ |
89 |
Destination address required |
需要目标地址 |
EMSGSIZE |
90 |
Message too long |
消息太长 |
EPROTOTYPE |
91 |
Protocol wrong type for socket |
socket协议类型错误 |
ENOPROTOOPT |
92 |
Protocol not available |
协议不可用 |
EPROTONOSUPPORT |
93 |
Protocol not supported |
不支持的协议 |
ESOCKTNOSUPPORT |
94 |
Socket type not supported |
套接字类型不受支持 |
EOPNOTSUPP |
95 |
Operation not supported on transport |
不支持的操作 |
EPFNOSUPPORT |
96 |
Protocol family not supported |
不支持的协议族 |
EAFNOSUPPORT |
97 |
Address family not supported by protocol |
协议不支持的地址 |
EADDRINUSE |
98 |
Address already in use |
地址已在使用 |
EADDRNOTAVAIL |
99 |
Cannot assign requested address |
无法分配请求的地址 |
ENETDOWN |
100 |
Network is down |
网络瘫痪 |
ENETUNREACH |
101 |
Network is unreachable |
网络不可达 |
ENETRESET |
102 |
Network dropped |
网络连接丢失 |
ECONNABORTED |
103 |
Software caused connection |
软件导致连接中断 |
ECONNRESET |
104 |
Connection reset by |
连接被重置 |
ENOBUFS |
105 |
No buffer space available |
没有可用的缓冲空间 |
EISCONN |
106 |
Transport endpoint |
传输端点已经连接 |
ENOTCONN |
107 |
Transport endpoint |
传输终点没有连接 |
ESHUTDOWN |
108 |
Cannot send after transport |
传输后无法发送 |
ETOOMANYREFS |
109 |
Too many references |
太多的参考 |
ETIMEDOUT |
110 |
Connection timed |
连接超时 |
ECONNREFUSED |
111 |
Connection refused |
拒绝连接 |
EHOSTDOWN |
112 |
Host is down |
主机已关闭 |
EHOSTUNREACH |
113 |
No route to host |
没有主机的路由 |
EALREADY |
114 |
Operation already |
已运行 |
EINPROGRESS |
115 |
Operation now in |
正在运行 |
ESTALE |
116 |
Stale NFS file handle |
陈旧的NFS文件句柄 |
EUCLEAN |
117 |
Structure needs cleaning |
结构需要清洗 |
ENOTNAM |
118 |
Not a XENIX-named |
不是XENIX命名的 |
ENAVAIL |
119 |
No XENIX semaphores |
没有XENIX信号量 |
EISNAM |
120 |
Is a named type file |
是一个命名的文件类型 |
EREMOTEIO |
121 |
Remote I/O error |
远程输入/输出错误 |
EDQUOT |
122 |
Quota exceeded |
超出磁盘配额 |
ENOMEDIUM |
123 |
No medium found |
没有磁盘被发现 |
EMEDIUMTYPE |
124 |
Wrong medium type |
错误的媒体类型 |
ECANCELED |
125 |
Operation Canceled |
取消操作 |
ENOKEY |
126 |
Required key not available |
所需键不可用 |
EKEYEXPIRED |
127 |
Key has expired |
关键已过期 |
EKEYREVOKED |
128 |
Key has been revoked |
关键被撤销 |
EKEYREJECTED |
129 |
Key was rejected by service |
关键被拒绝服务 |
EOWNERDEAD |
130 |
Owner died |
所有者死亡 |
ENOTRECOVERABLE |
131 |
State not recoverable |
状态不可恢复 |
ERFKILL |
132 |
Operation not possible due to RF-kill |
由于RF-kill而无法操作 |
3.5 输出错误原因
errno只是一个整型值,必须查表才能知道,要想更加方便地查找错误,可以利用两个函数,这两个函数提供了错误号到信息的转换:strerror和perror。strerror函数的原型:
#include <string.h>
char *strerror(int errnum);
详细见linux函数参考手册。perror函数的原型:
#include <stdio.h>
void perror(const char *s);
详细见linux函数参考手册。
注意:该字符串不要加‘\n’,系统会自动加。这样的好处是可以少传送一个参数;坏处是perror是无缓冲的,是一个有副作用的函数,其职能是输出离该函数调用最近的一个系统函数的出错原因。
4 全局跳转
goto语句是一个只能在函数内部跳转的语句,即这种跳转是局部的,对于全局跳转,goto语句是无力的。要想全局跳转,则需要那种全局跳转的语句。
linux下使用setjump函数和longjump函数实现全局跳转。这种跳转的思路是先设置一个跳转点,保存当前的函数调用栈帧。当程序执行全局跳转,回到跳转点的时候,要实现函数栈帧的还原。linux下使用jmp_buf结构保存当前的栈帧,再跳转的时候将该结构中的栈帧还原即可。linux下使用setjmp函数设置一个全局的跳转点,函数原型如下:
#include <setjmp.h>
int setjmp(jmp_buf env);
详细见linux函数参考手册。接下来 使用 longjmp函数执行全局跳转,即直接跳转到 setjmp的后一句,无论是在哪里,也无论是嵌套了几层函数,都能跳转回来,函数原型如下:
#include <setjmp.h>
void longjmp(jmp_buf env, int val);
详细见linux函数参考手册。使用全局跳转,程序的结构更好控制,代码也会变得紧凑。使用全局跳转是一个比较高级的应用,全局跳转需要操作系统的协助,而局部跳转不需要。局部跳转实现在语言层面上,而且注意goto只是C语言的一个关键字。