Linux C 系统编程(06)进程管理 进程环境

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 代码段、数据段与缓冲段

@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,因为是全局变量。

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函数参考手册。linux下好似用longjmp函数执行全局跳转,函数原型如下:

#include <setjmp.h>
void longjmp(jmp_buf env, int val);

详细见linux函数参考手册。使用全局跳转,程序的结构更好控制,代码也会变得紧凑。使用全局跳转是一个比较高级的应用,全局跳转需要操作系统的协助,而局部跳转不需要。局部跳转实现在语言层面上,而且注意goto只是C语言的一个关键字。

发布了289 篇原创文章 · 获赞 47 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/vviccc/article/details/105152197