进程是一段具有独立功能的程序在特定数据集合上一次动态执行的过程。它是系统进行资源分配和管理的独立单位,也是系统调度和执行的最小单位。
进程的特性有: 并发性、动态性、独立性、交互性
进程的种类有: 交互式进程、批处理进程、守护进程
本节将简要介绍进程的环境,主要包括进程的属性,环境变量,地址空间,以及进程环境相关的操作。
1.1 进程的基本属性
Linux系统中进程的属性包含在一个叫做进程控制块(PCB)结构体中。内核通过一个双向链表维护系统中所有的进程信息,每个链表节点就是一个PCB 结构体,其定义为 task_struct:
struct task_struct{
volatile long state;
pid_t pid;
pid_t tgid;
……………
};
PCB是由内核维护的数据结构,其中每个成员都代表着进程的某种属性。下面我们将对PCB中部分成员简单介绍。
1.1.1 进程标识符
进程标识符在内核中由PID标识,PID是一个非负整数,在系统中是唯一的,但又是可复用的,进程消亡后该ID 就可以被赋予其它进程。其在PCB中的表示如下:
pid_t pid;
pid_t tgid;
Glibc中定义了一些api 可以用来获取各种pid:
#include<unistd.h>
pid_t getpid(void) //返回调用进程的PID
pid_t getppid(void) //返回调用进程的父进程PID
pid_t getuid(void) //返回调用进程的实际用户ID
pid_t geteupid(void) //返回调用进程的有效用户ID
pid_t getgid(void) //返回调用进程的实际组ID
pid_t getegid(void) //返回调用进程的有效组ID
1.1.2 进程状态
进程从被创建到消亡,在内核中由一个状态机表示其各种状态,进程状态的在PCB 中的表示为:
volatile long state;
int exit_state;
进程的状态:
#define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2
#define TASK_ZOMBIE 4
#define TASK_STOPPED 8
- TASK_RUNNING:正在被执行,或者已经获得资源等待被执行;
- TASK_INTERRUPTIBLE:表示进程被阻塞,等到条件为真,进程状态转换为TASK_RUNNING。这个条件可能是获取到某些资源(wake_up)或者收到信号;
- TASK_UNINTERRUPTIBLE:与TASK_INTERRUPTIBLE类似,唯一的区别是会忽略信号;
- TASK_ZOMBIE:进程已经被终止,但是父进程还没有使用wait()等系统调用来获知它的终止信息,PCB 没有被释放;
- TASK_STOPPED:进程接收到一个SIGSTOP信号后就将运行状态改成TASK_STOPPED状态,然后在接收到SIGCONT信号时又恢复继续运行,多用于调试。
此外,PCB中还有很多信息,用来表示信号控制的,文件系统的等等,后面的章节中我们将逐一介绍。
1.2 进程环境
我们编写的程序都是从main函数开始执行的,那main函数又是如何被执行到的呢?其大概的过程是,链接器会将一个启动例程设置为起始地址,内核在执行C程序时,会从这个启动例程开始;启动例程从内核取得命令行参数与环境变量等值传递给main 函数可以执行。本节我们就来介绍下进程的环境表。
每个进程都维护一个独立的环境变量表,环境表是一个字符指针数组,其中每个指针项都是一个环境变量,它是一个以null 结束的字符串。环境表也是以一个NULL字符串结束。字符串的格式为:NAME=value。环境表格式如下图所示:
- 进程初始的环境表继承自父进程(可以通过参数传递或者全局变量两种方式从父进程获取);
- 每个进程都有一个独立的环境表,设置的环境变量只影响当前进程的,并不会影响到其它进程的环境变量表;
- 每个进程有独自的数据段,所以对应的environ 指针指向的是各自的环境表。
进程中对环境变量的操作一般是通过一组函数去访问而不是直接访问environ 指针,虽然实际上是可以这么做的。环境变量相关的操作函数如下:
头文件:stdlib.h
(1). 获取指定环境变量的值:
char *getenv(const char *name); |
参数:需要获取的环境变量name |
返回值:与name 关联的value指针,不存在 返回NULL |
(2). 设置环境变量值:
int *setenv(const char *name, const char *value, int rewite); |
参数:如rewite = 0,替代原有的value值;否则不替换,也不报错 |
返回值:0:success 非0:unsuccess |
int *putenv(const char *str); |
参数:str格式为name=value, 若name已经存在,替换原来的定义 |
返回值:0:success -1:unsuccess |
(3). 删除环境变量值:
char *unsetenv(const char *name); |
参数:str格式为name=value, 若name已经存在,替换原来的定义 |
返回值:0:success -1:unsuccess |
环境表位于进程存储空间的顶部(不是一定的),类似于链表的形式存储。那修改环境表是如何实现的呢?当然也是类似于对链表的处理。
- 删除一个表现很简单,与删除一个链表中节点的方式相同。
- 修改一个表现也很简单,如果新的value 小于原先的value直接替换即可。反之,须用malloc 分配新的空间,针对这一表现的指针指向新分配区。
- 增加一个表现那就复杂多了:
a. 如果是第一次新增,需要重新分配内存空间,将新的表现置于表尾,并以NULL 结尾,再使environ 指向新的地址。如果不幸的环境表位于顶部,那空间没有办法再增加啦,将它移到heap中吧。
b. 如果不是第一次新增,直接调用realloc 增加空间,并将该表项放至表尾,并以NULL结束。
Linux中可以用export 命令去获取,设置环境变量。
1.3 进程地址空间
Linux中每个进程都有自己的内存空间,当然这个内存空间只是虚拟地址空间,内存管理模块会通过页表的方式将虚拟空间映射到实际的物理地址。32bit的系统中这个虚拟地址空间大小为4G,其中kernel space和 user space的比例为1:3 。 kernel space 的1G 又可以被理解为所有进程共享的。其具体的组织形式如下图:
关于进程地址空间简单的说明如下:
- 代码段:CPU 执行的机器指令部分,只读,进程间可共享。这里的共享并不是说所有的进程拥有一份某段程序。而主要是体现在链接库中,程序在运行后再确定代码所在的内存地址,这样就能实现多个程序调用同一段代码;
- BSS段:程序加载时BSS会被操作系统清零,所以未赋初值或初值为0的全局变量都在BSS中。BSS段仅为未初始化的静态分配变量预留位置,并不分配实际的内存空间,这样可减少目标文件体积。当加载器(loader)加载程序时,将为BSS段分配内存并初始化为0。在嵌入式软件中,进入main()函数之前BSS段被C运行时系统映射到初始化为全零的内存。BSS段包括未赋初值或者初值为0的全局变量或静态局部变量。所以可以将BSS 段理解为只是保存信息;
- 数据段:初值不为0的全局变量或静态局部变量。比如定义一个int a =10 的全部变量,会在目标文件数据段中定义这个变量,并在程序load的时候复制到相应内存;
- 堆栈:堆由用户程序控制,例如 malloc 分配的内存地址。栈由编译器决定,主要存放非静态局部变量和函数调用过程的信息(栈帧),如的参数、返回地址和寄存器的值。Linux中ulimit -s命令可查看和设置堆栈最大值。
Linux 中可以用size 命令查看正文段、数据段的长度。
1.4. 进程环境相关的操作函数
(1). 分配存储空间
头文件: #include<stdlib.h> 分配失败返回NULL
void *malloc(size_t size); |
分配指定大小的存储区,初值未定 |
void *calloc(size_t nobj, size_t size); |
分配指定数据(nobj),指定大小的存储空间,并初始化为0 |
void *realloc(void *ptr, size_t newsize); |
增加或减少以前分配的存储区至newsize 大小,如果是增加存储区,可能需要将以前分配区的内容移动到新的存储区,新增的内容初值未定 |
void free(void *ptr); |
释放已分配的存储区,注意以前分配的存储区对应地址不能有变 |
(2). 跳转功能函数
下面介绍一组可以实现跨函数跳转的功能函数,这恰好可以弥补goto 语句的不足。
头文件: #include<setjmp.h> ()注意哦,不是setjump.h)
int setjmp(jump_buf env); |
第一次调用返回0,如从longjump返回,则为val |
void longjmp(jmp_buf env, int val); |
释放已分配的存储区,注意以前分配的存储区对应地址不能有变 |
这两个函数不同于goto函数,可以实现跨函数的跳转。使用也很简单,在需要跳转的点调用setjump,此时会将栈帧信息存放到env中,并返回0。在发生需要调回的点调用longjump,此时程序会根据env 中的信息跳转回setjump的点,而此时又会执行一遍setjump,并返回val。
一个简单的例子:
1. #include<stdio.h>
2. #include<stdlib.h>
3. #include<string.h>
4. #include<setjmp.h>
5.
6. static jmp_buf env;
7. static int globval;
8.
9. static void f1(int globval, int autoval, int regival,
10. int volval, int staval);
11.
12. int main(int argc, char *argv[])
13. {
14. int autoval;
15. register int regival;
16. volatile int volval;
17. static int staval;
18.
19. globval = 1;
20. autoval = 2;
21. regival = 3;
22. volval = 4;
23. staval = 5;
24.
25. if (setjmp(env) != 0)
26. {
27. printf("After longjump\n");
28. printf("value: %d %d %d %d %d\n", globval,
29. autoval, regival, volval , staval);
30. getchar();
31. }
32.
33. globval = 11;
34. autoval = 12;
35. regival = 13;
36. volval = 14;
37. staval = 15;
38.
39. f1(globval, autoval, regival, volval, staval);
40. printf("Can program come to here ???\n");
41. exit(0);
42. }
43. static void f1(int globval, int autoval,
44. int regival, int volval, int staval)
45. {
46. printf("Before longjump\n");
47. printf("value: %d %d %d %d %d\n", globval, a
48. utoval, regival, volval, staval);
49. longjmp(env,1);
50. printf("Can program come to f1 end ???\n");
51. }
执行结果如下:
(3).查询和更改资源限制
Linux系统中有资源的概念,比如,CPU,memory,stack等都是系统的资源。而下面的这组函数可以用来获取或设置相应的资源信息。
#include<sys/resource.h>
struct rlimit{
rlimit _t rlim_cur;
rlimit _t rlim_maxr;
};
int getrlimit(int resource, struct rlimit *rlptr); |
获取对应资源的值 |
int setrlimit(int resource, const struct rlimit *rlptr); |
设置对应资源的值 |
Linux 中可以操作的主要资源如下:
RLIMIT_AS | 进程可用存储区的最大总长度,会影响sbrk函数和mmap函数 |
RLIMIT_CORE | core文件的最大字节数,若其值为0则阻止创建core文件 |
RLIMIT_CPU | CPU时间的最大量值(秒),当超过此软限制时,向该进程发送SIGXCPU信号 |
RLIMIT_DATA | 数据段的最大字节长度。这是初始化数据、非初始化数据以及堆的总和 |
RLIMIT_FSIZE | 可以创建的文件的最大字节长度。当超过此软限制时,则向该进程发送SIGXFSZ信号 |
RLIMIT_LOCKS | 一个进程可持有的文件锁的最大数(此数也包括Linux特有的文件租借数) |
RLIMIT_MEMLOCK | 一个进程使用mlock能够锁定在存储器中的最大字节长度 |
RLIMIT_NOFILE | 每个进程能打开的最大文件数。更改此限制将影响到sysconf函数在参数_SC_OPEN_MAX中的返回值 |
RLIMIT_NPROC | 每个实际用户ID可拥有的最大子进程数。更改此限制将影响到sysconf函数在参数_SC_CHILD_MAX中返回的值 |
RLIMIT_RSS | 最大驻内存集的字节长度(resident set size in bytes, RSS)。如果物理存储器供不应求,则内核将从进程处取回超过RSS的部分 |
RLIMIT_SBSIZE | 用户在任一给定时刻可以占用的套接字缓冲区的最大长度(字节)。(Linux 2.4.22不支持) |
RLIMIT_STACK | 栈的最大字节长度 |
简要说明:
- 任何一个进程都可以将一个soft limit值更改为小于其hard limit值
- 任何一个进程都可以将一个hard limit值更改为大于其soft limit 值
- 只有超级用户才能增加hard limit值
- Linux中可以使用shell内置的ulimit命令更改这些资源
- 资源限制影响到调用进程并由其子进程继承
Linux中也可以使用ulimit命令去获取和设置资源信息,关于ulimit用法如下:
ulimit [options] [resource]
其中option选项有:
- -a 显示目前资源限制的设定;
- -H 设定资源的硬性限制,也就是管理员所设下的限制
- -S 设定资源的弹性限制
resource选项有:
- -c <core文件上限> 设定core文件的最大值,单位为区块
- -d <数据节区大小> 程序数据节区的最大值,单位为KB
- -f <文件大小> shell所能建立的最大文件,单位为区块
- -m <内存大小> 指定可使用内存的上限,单位为KB
- -n <文件数目> 指定同一时间最多可开启的文件数
- -p <缓冲区大小> 指定管道缓冲区的大小,单位512字节
- -s <堆叠大小> 指定堆叠的上限,单位为KB
- -t <CPU时间> 指定CPU使用时间的上限,单位为秒
- -u <程序数目> 用户最多可开启的程序数目
- -v <虚拟内存大小> 指定可使用的虚拟内存上限,单位为KB
1.5. 进程和中断上下文
1. 进程上下文
进程上文是指:进程由用户态切到内核态时需要保存用户态时寄存器的值,进程状态,堆栈上的内容,以便返回用户态时恢复;进程下文是指:进入内核态后执行的程序。
2. 中断上下文
中断上文是指:硬件通过中断触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。中断上文可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境;中断下文是指:中断服务程序。
本节内容转载自https://blog.youkuaiyun.com/qq_38500662/article/details/80598486
写在文末:本文作为个人对APUE的学习笔记,章节安排和内容基本参考APUE。文中有疏漏或者错误的地方,还请不吝赐教。