背景
本章节主要用于介绍Linux进程环境,其中包括进程的终止方式、环境变量、程序的存储空间布局、非局部跳转。为后续进程控制章节做铺垫。
进程的终止方式
进程终止的方式分为两种:正常终止、异常终止(本章暂不介绍);其中正常终止有5种情况。
从main
函数返回;
int main()
{
...
return 0;
}
调用exit
函数。
在任意线程中,调用exit
,会先执行一些清理动作,再返回至内核,从而结束进程。
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("hello world");
sleep(3);
exit(0);
}
输出如下:
调用_exit
或_Exit
它们与exit
的区别是,不执行清理动作,而是直接返回内核。
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("hello world");
sleep(3);
_exit(0);
}
输出如下:
最后一个线程从其启动例程返回;
针对这一点,在linux环境中,我觉得和第一点是相同的。main
函数也可以称为"主线程",进程中的最后一个线程”一定“是主线程。因为若main
函数先退出,就会触发第一点。有不同理解的朋友,还请指正。
从最后一个线程调用pthread_exit
;
同上理,当main
函数是最后一个线程时,调用pthrad_exit
也可退出。
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
int main()
{
printf("enter hello world\n");
pthread_exit(0);
printf("exit hello world\n");
}
输出如下:
下图描述了一个C程序在正常场景下,如何启动和终止的:
由图可知:
exit
函数,在退出用户进程前,会先执行终止处理程序(类似C++中的析构函数),再清理标准I/O;_exit和_Exit
函数,不会做任何应用层处理,直接将执行权交给内核。
其中终止处理程序是可以通过atexit
接口注册:
#include<stdlib.h>
int atexit(void (*func)(void));
演示demo如下:
#include <stdio.h>
#include <stdlib.h>
static void my_atexit(void)
{
printf("enter my_atexit");
}
int main()
{
printf("enter main\n");
if(atexit(my_atexit) != 0)
{
printf("register my_atexit failed\n");
}
printf("exit main\n");
return 0;
}
输出如下:
xieyihua@xieyihua:~/test$ ./7
enter main
exit main
enter my_atexitxieyihua@xieyihua:~/test$
注:一个进程最多注册32个终止处理函数,并且exit调用顺序与它们的注册顺序相反(栈数据结构)
环境表及环境变量
每个进程都有一张环境表,它是一个字符指针数组,以最后一个成员NULL为终止。环境表是通过全局变量extern char** environ;
进行保存的。我们可以通过以下示例,打印出进程的环境表。
#include<stdlib.h>
#include<stdio.h>
extern char** environ;
int main()
{
for(int i = 0 ; environ[i] != NULL; i++)
{
printf("%s\n",environ[i]);
}
return 0;
}
输出如下:
xieyihua@xieyihua:~/test$ ./8
SHELL=/bin/bash
PWD=/home/xieyihua/test
LOGNAME=xieyihua
XDG_SESSION_TYPE=tty
MOTD_SHOWN=pam
HOME=/home/xieyihua
LANG=en_US.UTF-8
SSH_CONNECTION=192.168.6.1 51016 192.168.6.128 22
LESSCLOSE=/usr/bin/lesspipe %s %s
XDG_SESSION_CLASS=user
TERM=xterm
LESSOPEN=| /usr/bin/lesspipe %s
USER=xieyihua
DISPLAY=localhost:10.0
SHLVL=1
XDG_SESSION_ID=959
XDG_RUNTIME_DIR=/run/user/1000
SSH_CLIENT=192.168.6.1 51016 22
XDG_DATA_DIRS=/usr/local/share:/usr/share:/var/lib/snapd/desktop
PATH=/home/xieyihua/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
SSH_TTY=/dev/pts/6
OLDPWD=/home/xieyihua
_=./8
xieyihua@xieyihua:~/test$
通常情况下,我们是通过getenv
和putenv
来访问特定的环境变量,而不是通过全局变量。
#include <stdlib.h>
char* getenv(const char* name);
int putenv(char* str);
实际上述函数操作的都是extern char** environ;
全局变量,不同的场景,其实现流程不太一样。因为环境表environ
和环境变量字符串,通常保存在进程空间的顶部(栈之上、内核空间之下)。使得表的大小无法变化。
- 如果修改一个现有的
name
:
- 如果新
value
的长度少于或等于现有value的长度,则只要将新字符串复制到原来的字符串所在空间即可。 - 如果新
value
的长度大于原来长度,则调用malloc
创为新值分配空间,将新的字符串空间保存更新到环境表中。
- 新增一个环境变量字符串
- 由于环境表无法在原来的基础上增加(虚拟地址空间限制了)。所以必须
malloc
一个新的环境表,将原先的环境表内容及新的环境变量拷贝其中,再将该堆地址复制给environ
。
如下示例:
#include<stdlib.h>
#include<stdio.h>
extern char** environ;
int main()
{
printf("environ=%p\n",environ);
putenv("xieyihua=hello world");
printf("environ=%p\n",environ);
return 0;
}
输出如下,全局变量环境表environ
的地址的确发生了变化:
xieyihua@xieyihua:~/test$ ./9
environ=0x7ffef6e484d8
environ=0x5632b3ad16b0
xieyihua@xieyihua:~/test$
C程序的存储空间布局
这是一直老生常谈的话题,C程序主要包含正文段(代码段)、初始化数据段(数据段)、未初始化数据段(bss段)、栈、堆等几个部分组成。
- 正文段:这是由CPU执行的机器指定部分。通常,正文段是可共享的,即使一个进程中多线程访问同一代码段,内存中也只有一个副本。因此,正文段应该也是只读权限。
- 初始化数据段:它主要存储代码中明确赋初值的全局变量或静态变量。
- 未初始化数据段:它主要存储代码中未赋值或赋值为0的全局变量或静态变量。
- 栈:它主要存储局部自动变量及每次函数调用时所需保存的信息都存放在此段中。
- 堆:进行动态存储分配。
存储空间大致如下:
注:未初始化数据段的内容并不存放在磁盘程序文件中
若想进一步了解相关内容,可参考我的专栏:编译,链接,装载,库
非局部跳转
在C语言中,goto
语句是不能跨函数的,而执行这类跳转功能的是函数setjmp
和longjmp
。在一些特殊场景下:处理发生在很深层嵌套函数调用中的出错情况是非常有用的。
关于这两个函数的原理和使用方式可参考我之前的文章:一文搞懂系列——非局部跳转setjmp和longjmp使用及原理
总结
本章内容主要介绍了Linux进程环境的相关知识,包括以下几个方面:
- 进程的终止方式:分为正常终止和异常终止。正常终止包括从main函数返回、调用exit函数、调用_exit或_Exit函数、最后一个线程从其启动例程返回、从最后一个线程调用pthread_exit。其中,exit函数在退出前会执行一些清理动作,而_exit和_Exit函数则直接返回内核。
- 环境表及环境变量:每个进程都有一张环境表,用于保存环境变量。环境表可以通过全局变量extern char** environ;访问。环境变量可以通过getenv和putenv函数进行操作。
- C程序的存储空间布局:主要包括正文段(代码段)、初始化数据段(数据段)、未初始化数据段(bss段)、栈、堆等部分。
- 非局部跳转:C语言中的goto语句不能跨函数,而setjmp和longjmp函数可以实现跨函数的跳转,适用于处理深层嵌套函数调用中的出错情况。
若我的内容对您有所帮助,还请关注我的公众号。不定期分享干活,剖析案例,也可以一起讨论分享。
我的宗旨:
踩完您工作中的所有坑并分享给您,让你的工作无bug,人生尽是坦途