目录
  1. 1. UNIX进程的环境
    1. 1.1. 了解main函数
    2. 1.2. 进程终止
      1. 1.2.1. 终止方式
        1. 1.2.1.1. 正常终止
        2. 1.2.1.2. 异常终止
    3. 1.3. C程序的存储空间布局
    4. 1.4. 静态库
    5. 1.5. 共享库
    6. 1.6. 存储器分配
      1. 1.6.1. 存储空间动态分配函数
    7. 1.7. 环境变量
      1. 1.7.1. 环境变量函数
      2. 1.7.2. 函数修改环境变量的过程
        1. 1.7.2.1. 位置
        2. 1.7.2.2. 删除字符串
        3. 1.7.2.3. 增加字符串(由于栈以上空间已在进程存储空间顶部,无法扩充)
    8. 1.8. setjmp和longjmp函数
    9. 1.9. 自动变量问题
    10. 1.10. getrlimit和setrlimit函数
    11. 1.11. 自言自语
APUE学习笔记-UNIX进程的环境

UNIX进程的环境

了解main函数

  • main函数原型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

/*******************************************
int argc : 命令行参数个数
char *argv[] : 命令行输入的所有参数依次放入数组中,argv[0]是命令本身
char *envp[] : 存储的是系统环境变量
notice : POSIX规定使用extern **environ而不使用envp参数
通常使用getenv函数和putenv函数来存储特定的环境变量而不是environ
如果要查看整个环境,只能使用environ指针
********************************************/
int main(int argc , char *argv[] , char *envp[]);

/*
应用extern
*/
#include <stdio.h>
extern char **environ ;
int main(void)
{
while(*environ)
{
printf("%s\n",*environ); /* 打印一行字符串 */
*environ++ ; /* 一行行的增加 */
}
return 0 ;
}
  • main启动过程
    内核启动C程序时(使用exec启动,内核使程序执行的唯一方法是调用一个exec函数),在调用main前先调用一个特殊的起动例程(这个起动例程被指定为程序的起始地址),起动例程从内核取得命令行参数(argc , argv)和环境变量值(envp),然后为调用main函数作安排
    程序结束时,起动例程将调用main函数返回。调用形式:
1
exit(main(argc , argv));

一个C程序的起动和终止

一个C程序的起动和终止

进程终止

终止方式
正常终止
  • 从main返回
  • 调用exit(需要先执行一些标准IO库的清除处理,然后进入内核)
  • 调用_exit(立即进入内核)
异常终止
  • 调用abort
  • 由信号终止

exit和_exit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
注意:main调用exit函数时带终止状态,则能识别主进程是终止状态,从而函数停止;如果不带终止状态,有些没有返回值的main函数的终止状态则未定义
使用exit或者_exit代替return,可以使得main返回void而不是int时,然后仍旧调用exit,可以避免不必要的警告
*/
#include <stdlib.h>
/*******************************************
int status : 终止状态
return : 无返回值
notice : 调用时,需要先清除标准IO库的调用(exit是ANIC C标准函数),再进入内核
********************************************/
void exit(int status);

#include <unistd.h>
/*******************************************
int status : 终止状态
return : 无返回值
notice : 调用时,立即进入内核
********************************************/
void _exit(int status);

atexit函数用来登记终止处理程序(一个进程可以登记多至32个函数,这些函数由exit自动调用)

1
2
3
4
5
6
7
8
#include <stdlib.h>
/*******************************************
void (*func)(void) : 一个函数指针,指向需要被登记的函数
return : 成功返回0,出错返回非0
function : 通过函数指针,登记函数指针指向的函数,登记后返回状态码
notice : exit以登记这些函数的相反顺序调用它们。同一函数登记多次,则也被调用多次
********************************************/
int atexit(void (*func)(void));

atexit应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include "../../include/apue.h"
#include "../../include/standard_error.h"
static void my_exit1(void) , my_exit2(void) ;
/***********************************************
程序首先打印main is done , 之后调用atexit来注册函数,
并将这些函数压栈,依次从前往后压栈。
最终,main函数调用exit函数返回值,exit函数依次从栈中
取出被注册的函数,依次销毁,最终返回状态码
因此,程序运行为:
main is done
can't register my_exit2
can't register my_exit2
can't register my_exit1
************************************************/
int main(void)
{
if(atexit(my_exit1) != 0)
{
err_sys("can't register my_exit1");
}

if(atexit(my_exit2) != 0)
{
err_sys("can't register my_exit2");
}
if(atexit(my_exit2) != 0)
{
err_sys("can't register my_exit2");
}
printf("main is done\n");
return 0 ;
}
static void my_exit1(void)
{
printf("first exit handler\n");
}
static void my_exit2(void)
{
printf("second exit handler\n");
}

C程序的存储空间布局

C程序由下面几部分组成

  • .text
    文本段,保存的是代码(程序代码在存储器中的副本),只读,通常可以共享
  • .data
    初始化数据段,包含了程序中需要赋值的变量,为它们开辟空间存放,初始化后的全局变量和静态局部变量都放在这里,属于静态内存分配
  • .bss
    未初始化数据段,在程序开始执行之前,内核将此段初始化为0,未初始化的全局变量和静态局部变量都放在这里(也可以放在data中,但是会增加文件大小),属于静态内存分配
  • .rodata
    存放C中的字符串和#define定义的常量
  • heap
    堆用来存放进程运行中被动态分配的内存段,可以动态扩张和缩减,我们使用malloc函数就是在堆中取空间,使用free函数释放这些分配的空间
    在32位下,堆最大可分配2.9G(程序逻辑地址大小,是虚拟的内存空间大小)
  • stack
    自动变量(用户创建的临时变量)以及每次函数调用时所需保存的信息都放在此段中
    函数在被调用时,其参数会被压入栈中,调用结束后,函数返回值也会被存放到栈中
    可以把栈看做寄存交换临时变量的内存区
  • comment
    存放编译器版本信息
  • 其他的
    .ELF header: 它描述整个文件的基本属性,比如ELF文件版本,目标机器型号,程序入口地址等。
    .debug段: 包含调试信息。
    .line段:调试时的行号,即源代码行号与编译后指令的对应表。
    .hash段:符号哈希表。
    .strtab段:String Table :字符串表,用于存储ELF文件中用到的各种字符串。
    下图显示了段的典型安排(正文段从0开始,栈顶从0x7fffffff开始)
    典型的存储器安排

静态库

静态库在linux上以.a结尾,在windows上以.lib结尾
静态库的代码在编译时就拷贝到应用程序中,成为程序的一部分。这样可以节省编译时间,并且程序不需要依赖就能使用。但是这使得程序体积很庞大。
创建静态库
my_lib.h

1
2
3
4
5
#ifndef _my_lib_h_
#define _my_lib_h_
void welcome(void);
void outstring(const char *str);
#endif

my_lib.c

1
2
3
4
5
6
7
8
9
10
11
12
13
#include "my_lib.h"
#include <stdio.h>
void welcome(void)
{
printf("Welcome to libmylib\n");
}
void outstring(const char *str)
{
if(str != NULL)
{
printf("%s\n",str);
}
}

编译成静态库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
gcc -o my_lib.o -c my_lib.c /* 将c文件编译为obj文件 */
/**************************************
r :表示将.o目标文件加入到静态库中(在已经存在的前面增加);
b :同r一样,但是在已经存在的后面增加
c :表示创建静态库;
s :表示生产索引;
d :从库中删除成员文件
m :在库中移动成员文件
p :在终端打印库中指定的成员
q :增加新文件到库的结尾而不检查是否有相同文件
t :显示库中成员清单
x :从苦衷提取一个成员文件
i :类似b选项
s :强制重新生成库符号表
***************************************/
ar rcs libmylib.a mylib.o /* 可以把多个.o文件封装为.a静态库 */

测试函数test.c

1
2
3
4
5
6
7
8
#include "my_lib.h"
#include <stdio.h>
int main(void)
{
printf("create and use library\n");
welcome();
outstring("It's successful.\n");
}

使用静态库

1
2
3
4
5
6
/**************************************
-Ldir:表示指定库文件所在的路径中,默认在库路径在/usr/lib目录下;
-lname:表示库目录的库文件libname.a或libname.so。如果库文件不是以lib开头,如hello.a,只能用用hello.a,不能用-lhello。
删除静态库文件不会影响到可执行文件的运行。
***************************************/
gcc -o test test.c -lmylib /* -l为选项,mylib为静态库名中间部分,linux上约定库以前缀lib开始,静态库以.a结尾,动态库以.so结尾 */

共享库

共享库和动态库就是一个东西(在linux上叫共享对象库,如:libglibc.so.6;在windows上叫动态加载函数库,以.dll结尾)
用动态连接方法将程序和共享库连接,减少了文件大小,但增加了运行时开销
共享库的另一个优点是可以用库函数的新版代替旧版而不需要程序重新编辑连接

共享库命名规则
创建共享库

1
2
3
4
5
6
gcc -fPIC -o mylib.o -c my_lib.c
gcc -shared -o libttt.so mylib.o
/*
两条命令可以合二为一
*/
gcc -fPIC -shared -o libttt.so my_lib.c

使用动态库

1
2
3
4
5
6
7
8
9
10
/*
两种方法之一
*/
gcc -o main test.c ./libttt.so

/*
两种方法之二
*/
cp libttt.so /usr/lib/libttt.so
gcc -o main test.c /usr/lib/libttt.so

存储器分配

存储空间动态分配函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <stdlib.h>
/*
realloc malloc free通过sbrk系统调用实现
大多数实现所分配的存储空间比所要求的要大一点,额外的空间用来记录管理信息(分配块的长度、指向下一个分配块的指针)
注意错误:
如果写过一个已分配区的尾端,会改写后一块的管理信息
释放一个已经释放了的块
调用free时所用的指针不是三个alloc函数的返回值

alloca函数
介绍:
其调用序列与malloc相同,在当前函数的栈帧上分配存储空间
优点:
当函数返回时,自动释放它所使用的栈帧,所以不必为释放空间而费心
缺点:
某些系统在函数调用后不能增加栈帧长度,因此不支持alloca函数
*/

/*******************************************
size_t size : 需要分配的内存大小(字节数)
return : 若成功则为非空指针,若出错则为NULL
function : 分配size个字节,最终返回一个指向被分配的内存空间的指针
notice : 如果size=0,malloc函数会返回NULL,或者一个能成功被free函数释放的非空指针值
********************************************/
void *malloc(size_t size);
/*******************************************
size_t nobj : 内存空间个数
size_t size : 需要分配的内存大小(字节数)
return : 若成功则为非空指针,若出错则为NULL
function : 在内存动态存储区中分配nobj个长度为size的连续空间
notice : 如果size或者nobj=0,malloc函数会返回NULL,或者一个能成功被free函数释放的非空指针值
如果nobj和size的乘法运算最终结果在int类型中溢出,那么calloc返回error
通过对比,整型溢出在之后调用malloc后不会被检测到,最终一个不正确的内存块将被分配
********************************************/
void *calloc(size_t nobj , size_t size);
/******************************************
void *ptr : 一个无类型的指针
size_t size : 需要分配的内存大小(字节数)
return : 若成功则为非空指针,若出错则为NULL
function : 为ptr指向的内存空间重新分配size内存大小(增、减以前分配的长度)
notice : 如果新增加的内存空间比以前的大,则新增内存不会被初始化
如果ptr=NULL,则等价于调用malloc(size)
如果size=0,ptr不为空,那么等价于free(ptr)
这个函数必须在之前就要调用malloc,calloc或者realloc函数来返回,除非ptr不为空
********************************************/
void *realloc(void *ptr , size_t size);
/*******************************************
void *ptr : 一个无类型的指针
return : 若成功则为非空指针,若出错则为NULL
function : 释放ptr指向的内存空间
notice : 如果ptr=NULL,不需要进行释放
********************************************/
void free(void *ptr);

环境变量

环境变量的形式是键值对
环境变量(char)=环境变量值(char)

环境变量函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdlib.h>

/*******************************************
const char *name : 环境变量名
return : 返回对应的环境变量值
function : 通过环境变量名获取环境变量值
********************************************/
char *getenv(const char *name);

/*******************************************
const char *str : 环境变量键值对字符串
return : 成功返回0,失败返回非0
function : 取形式为name=value的字符串,将其放到环境表中,如果name已存在,则删除其原来的定义
********************************************/
int putenv(const char *str);

/*******************************************
const char *name : 环境变量名
const char *value : 环境变量值
int rewrite : 标志,非0表示删除现存定义,0表示不删除现存定义
return : 成功返回0,失败返回非0
function : 将name设置为value,如果已经存在,按rewrite执行
********************************************/
int setenv(const char *name , const char *value , int rewrite);
/*******************************************
const char *name : 环境变量名
return : 无返回值
function : 删除指定环境变量名的环境变量定义
********************************************/
void unsetenv(const char *name);
函数修改环境变量的过程

环境变量表在程序运行后就被加载到程序中。

位置

在进程存储空间的顶部,也就是栈之上。

删除字符串

找到指向该字符串的指针,然后将所有后续指针都向下移一个位置(向下移表示最上面的被抛弃)

增加字符串(由于栈以上空间已在进程存储空间顶部,无法扩充)
  • 修改现存name
    新value<=旧value , 只需要在原字符串空间写入新字符串
    新value>旧value , 需要调用malloc函数为新字符串分配空间
  • 增加一个name(需要调用malloc为name=value分配空间)
    第一次增加一个新name,需要调用malloc为新的指针表分配空间,将原来的环境表复制到新分配区,并将新name=value的指针存在该指针表的表尾,然后将一个空指针存在气候,最后使environ指向新指针表。
    如果不是第一次增加,则只需要调用realloc,重新分配原空间,将name=value存入表尾,后面跟一个空指针。

setjmp和longjmp函数

  • 缘由
    在c中,不允许使用跳跃函数的goto语句。使用这两个函数来代替。

  • 使用
    setjmp和longjmp实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include "../../include/apue.h"
#include <setjmp.h>
#define TOK_ADD 5
void do_line(char *);
void cmd_add(void);
int get_token(void);
/* 这个程序有三个函数
主函数main首先入栈,之后调用do_line函数,do_line函数再入栈
之后do_line函数调用cmd_add函数,cmd_add入栈
之后cmd_add函数调用get_token函数,get_token函数入栈
如果在cmd_add函数中出现错误,返回main函数,我们需要检查返回值而逐层返回
解决这个问题可以用非局部跳转函数,类似goto语句,可以从cmd_add处直接跳到main函数处
*/
jmp_buf jmpbuffer ;

int main(void)
{
char line[MAXLINE];

/*******************************************
#include <setjmp.h>
int setjmp(jmp_buf env);
env : 一个数组,保存调用longjmp时能用来恢复栈状态的所有信息
function: 返回的是longjmp中的第二个参数值,用来判断是哪个跳跃点
notice : 如果返回0,表示没有错误出现
********************************************/
if(setjmp(jmpbuffer) != 0)
perror("error");
while(fgets(line , MAXLINE , stdin) != NULL)
do_line(line); /* 将数组中的命令提取出来进行解析 */

exit(0);
}
char *tok_ptr ; /* 针对get_token()的全局指针 */
void do_line(char *ptr)/* 处理一个输入行 */
{
int cmd ;
tok_ptr = ptr ;
while((cmd = get_token()) > 0) /* 传入分解后的命令 */
{
switch(cmd)
{
case TOK_ADD :
cmd_add();
break;
}
}
}
void cmd_add(void)
{
int token ;
token = get_token();
/* 处理命令的其余部分 */
if(token < 0)
{
/********************************************
#include <setjmp.h>
void longjmp(jmp_buf env , int val);
env :用来保存当前栈状态信息,给setjmp函数使用
val :可以看做是标记,因为一个程序中可能有对个跳跃点,这可以为每个跳跃点标示,使得使用setjmp函数时,可以知道是哪个跳跃点
********************************************/
longjmp(jmpbuffer , 1);
}
}
/* 从tok_ptr指向的line中获取下一个标记 */
int get_token(void)
{

}

/*
注意:
longjmp函数被调用,返回main函数,自动变量和寄存器变量不会回滚到第一次调用setjmp时的值(也就是未出现错误时的值),这时它们的值不确定
*/
**longjmp对自动,寄存器和易失变量的影响**
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <setjmp.h>
#include "../../include/apue.h"

/* 使用static是为了使在执行longjmp时其值保存不变*/
static void f1(int , int , int) ;
static void f2(void);

static jmp_buf jmpbuffer ;

/*
在进行跳转后,volatile(存储器)中的值不会回滚
而register(寄存器中的值会回滚
int类型中的值不稳定,优化之前不会回滚,优化之后,将int放入寄存器中,会回滚
*/

int main(void)
{
int count ; /* 存放到寄存器中 */
register int val ; /* 寄存器变量,存储到寄存器中 */
volatile int sum ; /* 存放到存储器中 */
count = 2 ;
val = 3 ;
sum = 4 ;
if(setjmp(jmpbuffer) != NULL)
{
printf("after longjmp : count=%d,val=%d,sum=%d\n",count , val , sum);
exit(0);
}

/* 在longjmp调用之前,setjmp调用之后改变变量值 */

count = 97;
val = 98 ;
sum = 99 ;
f1(count , val , sum); /* 没有返回值,使用longjmp跳转 */
}

static void
f1(int i , int j , int k)
{
printf("in f1(): counf=%d , val=%d , sum=%d\n",i,j,k);
f2();
}

static void
f2(void)
{
longjmp(jmpbuffer , 1);
}

自动变量问题

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#define DATAFILE "datafile"

/* open_data函数已经返回,他所在的栈帧会被其他函数使用
而标准库仍然使用在栈上被分配的流缓存,这回产生冲突
因此我们不能使用自动变量,在这里需要将其设置为静态全局
这样databuf变量存在全局静态区,在整个程序周期都存在,不会产生冲突
*/
static char databuf[BUFSIZ]; /* 自动变量,存储io流缓存 */
FILE *
open_data(void)
{
FILE *fp ;

if((fp = fopen(DATAFILE , "r")) == NULL) /* 用只读方式打开文件 */
{
return NULL ;
}

if(setvbuf(fp , databuf , _IOLBF , BUFSIZ) != 0) /* 设置行缓存 */
{
return NULL ;
}

return fp ;
}

getrlimit和setrlimit函数

  • 作用
    用于查询和更改进程资源限制(在linux中以ulimit命令显现)
  • 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <sys/time.h>
#include <sys/resource.h>

/*******************************************
int resource : 资源类型
struct rlimit *rlptr : 资源信息结构指针
function : 为指定资源设置一个指定的资源信息
notice :
rlimit结构体:
struct rlimit {
rlim_t rlim_cur ; //当前限制
rlim_t rlim_max ; //当前限制的最大值
}
资源类型:
RLIMIT_CORE : core文件最大字节数
RLIMIT_CPU : CPU时间的最大量值
RLIMIT_DATA : 数据段的最大字节长度
RLIMIT_MEMLOCK : 锁定在存储地址空间
RLIMIT_NOFILE : 每个进程能打开的最多文件数
RLIMIT_NPROC : 每个实际用户id所拥有的最大子进程数
RLIMIT_OFILE : 与RLIMIT_NOFILE相同
RLIMIT_FSIZE : 可以创建的文件的最大字节长度
RLIMIT_RSS : 最大驻内存集字节长度
RLIMIT_STACK : 栈的最大字节长度
RLIMIT_VMEN : 可映照地址空间的最大字节长度
********************************************/
int getrlimit(int resource , struct rlimit *rlptr);
int setrlimit(int resource , const struct rlimit *rlptr);
  • 使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <sys/types.h>
#include <sys/time.h>
#include <sys/resource.h>
#include "../../include/apue.h"


/* 这里将pr_limits函数作为常量来使用 */
#define doit(name) pr_limits(#name,name)

static void
pr_limits(char * , int);

int
main(void)
{
doit(RLIMIT_CORE);
doit(RLIMIT_CPU);
doit(RLIMIT_DATA);
doit(RLIMIT_FSIZE);

#ifdef RLIMIT_MEMLOCK
doit(RLIMIT_MEMLOCK);
#endif

#ifdef RLIMIT_OFILE
doit(RLIMIT_OFILE);
#endif

#ifdef RLIMIT_NPROC
doit(RLIMIT_NPROC);
#endif

#ifdef RLIMIT_RSS
doit(RLIMIT_RSS);
#endif

#ifdef RLIMIT_STACK
doit(RLIMIT_STACK);
#endif

#ifdef RLIMIT_VMEM
doit(RLIMIT_VMEM);
#endif

exit(0);
}

static void
pr_limits(char *name , int resource)
{
struct rlimit limit ; /* 进程限制结构体 */
if(getrlimit(resource , &limit) < 0) /* 从指定资源中获取其中的limit中的信息 */
{
err_sys("getrlimit error for %s",name);
}
printf("%-14s",name); /* 打印资源名称 */
if(limit.rlim_cur == RLIM_INFINITY) /* RLIM_INFINITY表示不对资源做限制 */
{
printf("(infinity) ");
}else
{
printf("%10ld ",limit.rlim_cur); /* 当前资源限制 */
}

if(limit.rlim_max == RLIM_INFINITY)
{
printf("(infinity) ");
}else
{
printf("%10ld ",limit.rlim_max);
}
}

自言自语

暂时了解了在Unix下的一些关于进程的基础知识。

文章作者: rack-leen
文章链接: http://yoursite.com/2019/05/05/APUE/APUE%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/APUE%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0-UNIX%E8%BF%9B%E7%A8%8B%E7%9A%84%E7%8E%AF%E5%A2%83/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 rack-leen's blog
打赏
  • 微信
  • 支付宝

评论