Linux C 系统编程 2-1 进程管理 进程环境

该系列文章总纲链接:专题分纲目录 LinuxC 系统编程​​​​​​​


本章节思维导图如下所示(思维导图会持续迭代):

第一层:

第二层:


1 进程的启动和退出

1.1 流程

程序启动 -> 程序加载 & 地址分配 -> 程序退出

@1 程序启动:对于二进制文件:

  1. 如果文件在usr/bin或者/bin文件夹下等在PATH环境变量中已经指定的地址中,则直接输入二进制文件的名字即可
  2. 若不在PATH环境变量下,则用 程序路径 程序名(例如./hello)的方式也可以。
  3. 若不在PATH环境变量下却想直接输入二进制名就能执行文件则将程序路径添加到PATH环境变量里面,将二进制文件移动到PATH环境变量的目录中。

@2 程序加载,地址分配:

加载的简单过程如下:

  1. 从目标文件中读取足够的头部信息,找出需要多少地址空间。
  2. 分配地址空间,如果目标代码的格式具有独立的段,那就将地址空间按独立的段来划分。
  3. 将程序读入地址空间的段中。
  4. 将程序末尾的bss段空间填充为0(如果虚拟内存系统部自动这么做的话)。
  5. 创建一个堆栈(若体系结构需要的话)。
  6. 设置运行信息,比如程序参数、环境变量等。
  7. 开始运行程序,从_start入口,找到main,开始顺序执行程序。

@3 程序退出:

一般退出有3种方式:

  1. 进程自愿退出。这体现在exit函数和return函数上。退出时,需要回收进程所分配的资源(比如地址空间、文件描述符等),操作系统会对每一项资源进行善后处理。    
  2. 进程收到一个信号退出。这种情况很常见,往往是父进程对其子进程的终止操作。这一操作实际上是父进程向子进程发送了一个终止信号,子进程接收到信号后也会资自愿退出。
  3. 进程执行了一个导致异常的操作后退出。上面两种情况都是在程序预期之下退出的,而异常的操作是在程序没有准备的情况下退出的。这时候操作系统对其资源进行回收,但是可能不会对这些资源进行善后处理。异常实际上是一种向进程发送了一个特殊的信号,但是发送信号的不是某一个进程而是操作系统本身。

1.2 进程终止处理函数

linux环境下允许在进程退出的时候调用一些用户自定义的函数,这些函数称为终止处理函数。linux规定最多可以设置32个这样的进程终止处理函数。linux下使用atexit函数设置进程终止处理函数。atexit函数的原型:

#include <stdlib.h>
int atexit(void (*function)(void));

详细见linux函数参考手册。注意:

  1. 函数执行成功返回0,失败返回非零值。(注意,atexit函数执行失败时不是返回-1)
  2. 进程终止处理函数的调用顺序与设置的时候相反。(后调用的先结束,类似于栈的结构)         
  3. 实际上,进程终止函数是在进程结束的时候进行的一些辅助性的操作。

2 linux进程内存管理

2.1 大端与小端

一般的PC机采用的是小端的结构,而server一般采用大端结构。这种数据存储的差别并不是由操作系统造成的,大端与小端体现在CPU的体系结构上。一般对其进行编程的时候要先判断是否是大端还是小端,之后对其进行操作。

小端:高地址存高位,低地址存低位。大端:与小端相反。

2.2 代码段、数据段与bss段

@1 代码段:一般是不允许进行写操作的,属性是只读。一个程序多数情况下不需要更改代码段,只有一种情况除外,那就是升级程序,对于server而言,要在不停止程序的情况下完成代码段部分内容的更换。以前一般就是对代码段进行写的操作,直接更换,但是这样风险也很大。现在一般用共享库的方式来解决这个问题。

@2 数据段:

  1. 初始化数据段(.data):包含程序中明确给定初值的全局变量和静态变量。
  2. 块存储段(.bss):存储在这个段中的数据通常是没有明确给定初值的全局变量和静态变量。

@3 bss段中的内容并不作为程序文件中的一部分,也就是不包含在二进制文件中,而是被保存在外存上,系统仅仅是在内存中标记了bss段的一些信息(初始化变量大小、属性等);以便于运行程序的时候能够找到bss段中的内容。如果全局变量/静态变量本身有给定的值,而这个值是0/NULL的时候,编译器会将其内容写到bss段,而不是data段中。

2.3 栈与堆

自动变量有3种存储方式:

  1. bss段:静态局部变量
  2. 寄存器里:寄存器变量
  3. 栈:一般自动变量

这里面在编程的时候最常见的错误就是将一个指向局部变量的指针作为函数的返回值返回。由于指针指向的内容还在栈帧上,函数只是将其地址返回。因此如果栈帧被其他函数覆盖,返回的指针指向的内存区域的值也就失效了。

堆空间一般是存储用户申请的内存空间,堆上的操作往往是malloc。栈与堆的位置往往是相对的,但是具体的分配要看处理器的存储结构,与大端小端的差别是类似的。

2.4 常量存储

对于一个简单的常量来说,它是存储在代码段里,因为简单变量的长度是固定的。这样可以加快取指令的速度,还可以提高程序的效率。但那是对于字符串这种复杂的常量而言,其长度不定,如果将字符串存储在代码段中,会导致代码段很大,同时不利于处理器进行代码读入缓冲处理,大大影响程序的执行效率。所以最后单独弄出一个段来存储字符串。

2.5 动态内存管理

系统使用mem_control_block结构管理所有已经分配的内存块,结构如下:

    struct mem_control_block{
      int is_available;     //该块是否可用
      int size;               //块的大小
    }

通过这个结构可以简单的实现malloc函数,整个流程如下:

malloc函数首先将用户要分配的字节数加上一个“内存控制块”的大小,得出实际需要分配的字节数。

  1. 之后顺序遍历堆中所有的内存块,如果该块可用且大于实际需要的字节数,则将该内存块的首地址返回并将该块设置为可用,否则尝试下一个内存块。
  2. 如果所有的内存块都不满足条件,则调用sbrk函数(如果sbrk函数失败,则系统中没有可用的内存了,malloc函数返回NULL),通过操作系统分配一块内存。malloc函数将这块内存拓展在堆内,相当于堆增长了。
  3. 跳过该块内存的“内存控制结构”,将最后一块内存的末地址重新设置。

对于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中情况:

  1. 程序中的main函数运行结束,$?中保存main函数的返回值。
  2. 程序运行中调用exit函数结束运行,$?保存的是exit函数的参数。
  3. 程序异常退出,$?中保存异常出错的错误号。

注意:

  1. 如果程序运行出错,那么$?内置变量中的值是1。所以在编写代码的时候,如果代码没有问题,则不要返回1(exit(1)或者return(1))。以免引起不必要的混乱。
  2. 如果main函数没有返回一个指定的值,那么$?中的值不是随机的,切记!
  3. 由于linux shell中$?中内置的变量的值实际上是进程结束后eax寄存器的值(仅在X86体系结构下),所以看到的是linux系统在此结构体系中使用eax保存每个函数的返回值。这个值针对不同的系统是不同的。

3.4 使用errno调试程序

调试一个程序的方法往往有以下几种:

  • 使用调试器
  • 在程序中直接使用输出函数输出调试信息
  • 查看标准出错文件
  • 程序异常时所写的日志

在linux下执行系统调用的时候会出现一些错误,仅仅通过检查这些系统调用的返回值是不够的,开发者往往需要更加详细的信息。C语言提供了一个全局变量errno,使用时加上头文件<errno.h>,这个全局变量很好地弥补了返回值信息不足的缺点。errno为0表示没有错误,如果出错则输出错误号。使用的时候一定要先清0,因为是全局变量。

常见errno的定义及解读如下表所示:

C NameValueDescription含义
Success0Success成功
EPERM1Operation not permitted操作不允许
ENOENT2No such file or directory没有这样的文件或目录
ESRCH3No such process没有这样的过程
EINTR4Interrupted system call系统调用被中断
EIO5I/O errorI/O错误
ENXIO6No such device or address没有这样的设备或地址
E2BIG7Arg list too long参数列表太长
ENOEXEC8Exec format error执行格式错误
EBADF9Bad file number坏的文件描述符
ECHILD10No child processes没有子进程
EAGAIN11Try again资源暂时不可用
ENOMEM12Out of memory内存溢出
EACCES13Permission denied拒绝许可
EFAULT14Bad address错误的地址
ENOTBLK15Block device required块设备请求
EBUSY16Device or resource busy设备或资源忙
EEXIST17File exists文件存在
EXDEV18Cross-device link无效的交叉链接
ENODEV19No such device设备不存在
ENOTDIR20Not a directory不是一个目录
EISDIR21Is a directory是一个目录
EINVAL22Invalid argument无效的参数
ENFILE*23File table overflow打开太多的文件系统
EMFILE24Too many open files打开的文件过多
ENOTTY25Not a tty device不是tty设备
ETXTBSY26Text file busy文本文件忙
EFBIG27File too large文件太大
ENOSPC28No space left on device设备上没有空间
ESPIPE29Illegal seek非法移位
EROFS30Read-only file system只读文件系统
EMLINK31Too many links太多的链接
EPIPE32Broken pipe管道破裂
EDOM33Math argument out of domain数值结果超出范围
ERANGE34Math result not representable数值结果不具代表性
EDEADLK35Resource deadlock would occur资源死锁错误
ENAMETOOLONG36Filename too long文件名太长
ENOLCK37No record locks available没有可用锁
ENOSYS38Function not implemented功能没有实现
ENOTEMPTY39Directory not empty目录不空
ELOOP40Too many symbolic links encountered符号链接层次太多
EWOULDBLOCK41Same as EAGAIN和EAGAIN一样
ENOMSG42No message of desired type没有期望类型的消息
EIDRM43Identifier removed标识符删除
ECHRNG44Channel number out of range频道数目超出范围
EL2NSYNC45Level 2 not synchronized2级不同步
EL3HLT46Level 3 halted3级中断
EL3RST47Level 3 reset3级复位
ELNRNG48Link number out of range链接数超出范围
EUNATCH49Protocol driver not attached协议驱动程序没有连接
ENOCSI50No CSI structure available没有可用CSI结构
EL2HLT51Level 2 halted2级中断
EBADE52Invalid exchange无效的交换
EBADR53Invalid request descriptor请求描述符无效
EXFULL54Exchange full交换全
ENOANO55No anode没有阳极
EBADRQC56Invalid request code无效的请求代码
EBADSLT57Invalid slot无效的槽
EDEADLOCK58Same as EDEADLK和EDEADLK一样
EBFONT59Bad font file format错误的字体文件格式
ENOSTR60Device not a stream设备不是字符流
ENODATA61No data available无可用数据
ETIME62Timer expired计时器过期
ENOSR63Out of streams resources流资源溢出
ENONET64Machine is not on the network机器不上网
ENOPKG65Package not installed没有安装软件包
EREMOTE66Object is remote对象是远程的
ENOLINK67Link has been severed联系被切断
EADV68Advertise error广告的错误
ESRMNT69Srmount errorsrmount错误
ECOMM70Communication error on send发送时的通讯错误
EPROTO71Protocol error协议错误
EMULTIHOP72Multihop attempted多跳尝试
EDOTDOT73RFS specific errorRFS特定的错误
EBADMSG74Not a data message非数据消息
EOVERFLOW75Value too large for defined data type值太大,对于定义数据类型
ENOTUNIQ76Name not unique on network名不是唯一的网络
EBADFD77File descriptor in bad state文件描述符在坏状态
EREMCHG78Remote address changed远程地址改变了
ELIBACC79Cannot access a needed shared library无法访问必要的共享库
ELIBBAD80Accessing a corrupted shared library访问损坏的共享库
ELIBSCN81A .lib section in an .out is corrupted库段. out损坏
ELIBMAX82Linking in too many shared libraries试图链接太多的共享库
ELIBEXEC83Cannot exec a shared library directly不能直接执行一个共享库
EILSEQ84Illegal byte sequence无效的或不完整的多字节或宽字符
ERESTART85Interrupted system call should be restarted应该重新启动中断的系统调用
ESTRPIPE86Streams pipe error流管错误
EUSERS87Too many users用户太多
ENOTSOCK88Socket operation on non-socket套接字操作在非套接字上
EDESTADDRREQ89Destination address required需要目标地址
EMSGSIZE90Message too long消息太长
EPROTOTYPE91Protocol wrong type for socketsocket协议类型错误
ENOPROTOOPT92Protocol not available协议不可用
EPROTONOSUPPORT93Protocol not supported不支持的协议
ESOCKTNOSUPPORT94Socket type not supported套接字类型不受支持
EOPNOTSUPP95Operation not supported on transport不支持的操作
EPFNOSUPPORT96Protocol family not supported不支持的协议族
EAFNOSUPPORT97Address family not supported by protocol协议不支持的地址
EADDRINUSE98Address already in use地址已在使用
EADDRNOTAVAIL99Cannot assign requested address无法分配请求的地址
ENETDOWN100Network is down网络瘫痪
ENETUNREACH101Network is unreachable网络不可达
ENETRESET102Network dropped网络连接丢失
ECONNABORTED103Software caused connection软件导致连接中断
ECONNRESET104Connection reset by连接被重置
ENOBUFS105No buffer space available没有可用的缓冲空间
EISCONN106Transport endpoint传输端点已经连接
ENOTCONN107Transport endpoint传输终点没有连接
ESHUTDOWN108Cannot send after transport传输后无法发送
ETOOMANYREFS109Too many references太多的参考
ETIMEDOUT110Connection timed连接超时
ECONNREFUSED111Connection refused拒绝连接
EHOSTDOWN112Host is down主机已关闭
EHOSTUNREACH113No route to host没有主机的路由
EALREADY114Operation already已运行
EINPROGRESS115Operation now in正在运行
ESTALE116Stale NFS file handle陈旧的NFS文件句柄
EUCLEAN117Structure needs cleaning结构需要清洗
ENOTNAM118Not a XENIX-named不是XENIX命名的
ENAVAIL119No XENIX semaphores没有XENIX信号量
EISNAM120Is a named type file是一个命名的文件类型
EREMOTEIO121Remote I/O error远程输入/输出错误
EDQUOT122Quota exceeded超出磁盘配额
ENOMEDIUM123No medium found没有磁盘被发现
EMEDIUMTYPE124Wrong medium type错误的媒体类型
ECANCELED125Operation Canceled取消操作
ENOKEY126Required key not available所需键不可用
EKEYEXPIRED127Key has expired关键已过期
EKEYREVOKED128Key has been revoked关键被撤销
EKEYREJECTED129Key was rejected by service关键被拒绝服务
EOWNERDEAD130Owner died所有者死亡
ENOTRECOVERABLE131State not recoverable状态不可恢复
ERFKILL132Operation 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语言的一个关键字。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

图王大胜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值