一、典型的Linux结构
最简单也是最复杂的操作:
二、进程的概念
1、进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
OS: 程序的一个执行实例。
正在执行的程序。
能分配处理器并由处理器执行的实体。
内核:担当分配系统资源(CPU时间,内存)的实体。
进程的两个基本元素是程序代码(可能被执行相同程序的其他进程共享)和代码相关联的数据集。进程是一种动态描述,但是并不代表所有的进程都在运行。(进程在内存中因策略或调度需求,会处于各种状态)。
2、狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。
广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
进程的概念主要有两点:第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。
3、进程的特征:
动态性:进程的实质是程序在多道程序系统中的一次执行过程,进程是动态产生,动态消亡的。
并发性:任何进程都可以同其他进程一起并发执行。
独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位。
异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进。
结构特征:进程由程序、数据和进程控制块三部分组成。
多个不同的进程可以包含相同的程序,一个程序在不同的数据集里就构成不同的进程,能得到不同的结果;但是执行过程中,程序不能发生改变。
4、进程的状态
进程执行时的间断性,决定了进程可能具有多种状态。事实上,运行中的进程可能具有以下三种基本状态。
1)就绪状态(Ready):
进程已获得除处理器外的所需资源,等待分配处理器资源;只要分配了处理器进程就可执行。就绪进程可以按多个优先级来划分队列。例如,当一个进程由于时间片用完而进入就绪状态时,排入低优先级队列;当进程由I/O操作完成而进入就绪状态时,排入高优先级队列。
2)运行状态(Running):
进程占用处理器资源;处于此状态的进程的数目小于等于处理器的数目。在没有其他进程可以执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。
3)阻塞状态(Blocked):
由于进程等待某种条件(如I/O操作或进程同步),在条件满足之前无法继续执行。该事件发生前即使把处理器资源分配给该进程,也无法运行。
5、区别
程序
程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。而进程是程序在处理机上的一次执行过程,它是一个动态的概念。
程序可以作为一种软件资料长期存在,而进程是有一定生命期的。程序是永久的,进程是暂时的。
进程更能真实地描述并发,而程序不能;
进程是由进程控制块、程序段、数据段三部分组成;
进程具有创建其他进程的功能,而程序没有。
同一程序同时运行于若干个数据集合上,它将属于若干个不同的进程,也就是说同一程序可以对应多个进程。
在传统的操作系统中,程序并不能独立运行,作为资源分配和独立运行的基本单元都是进程。
线程
进程和线程关系
进程和线程关系
通常在一个进程中可以包含若干个线程,它们可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统内多个程序间并发执行的程度。
当下推出的通用操作系统都引入了线程,以便进一步提高系统的并发性,并把它视为现代操作系统的一个重要指标。
控制
进程控制是进程管理中最基本的功能。它用于创建一个新进程,终止一个已完成的进程,或者去终止一个因出现某事件而使其无法运行下去的进程,还可负责进程运行中的状态转换。
二、进程描述
进程控制块
每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体。
task_struct是Linux内核的一种数据结构,它会被装载到RAM里并且包含着进程的信息。每个进程都把它的信息放在 task_struct 这个数据结构里,task_struct 包含了这些内容:
标示符 : 描述本进程的唯一标示符,用来区别其他进程。
状态 :任务状态,退出代码,退出信号等。
优先级 :相对于其他进程的优先级。
程序计数器:程序中即将被执行的下一条指令的地址。
内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
上下文数据:进程执行时处理器的寄存器中的数据。
I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
记账信息:可能包括处理器时间总和,使⽤用的时钟数总和,时间限制,记账号等。
保存进程信息的数据结构叫做 task_struct,并且可以在 include/linux/sched.h 里找到它。所有运行在系统里的进程都以 task_struct 链表的形式存在内核里。
更多内容见下述链接:
Linux下的进程描述
三、进程标识符
进程id(PID)—–可通过getid()函数获得
父进程id(PPID)—-可通过getppid()函数获得
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);
getpid()用来取得目前进程的进程识别码,许多程序利用取到的
此值来建立临时文件,以避免临时文件相同带来的问题。
返回值: 均为目前进程的进程识别码。
四、进程位置
1、进程的内存映像
Linux下C程序生成主要由四个步骤组成:预编译、编译、汇编、链接。编译器gcc经过预编译、编译、汇编3个步骤将源程序文件转换成目标文件。如果程序有多个目标文件或程序中使用了库函数,则编译器还需要将所有目标文件及所需的库文件链接起来,最后生成可执行程序。当程序执行时,操作系统将可执行程序复制到内存中,程序转为为进程通常需要以下步骤:
*内核将程序读入内存,为程序分配内存空间;
*内核为该进程保存PID及相应的状态信息,把进程放到运行队列中等待执行。程序转化为进程后就可被操作系统的调度程序执行了。
进程的内存映像是指内核在内存中如何存放可执行程序文件。在将程序转化为进程的过程中,操作系统将可执行程序从硬盘复制至内存中,其布局如下:
2、进程映像的位置依赖于使用的内存管理方案。
3、可执行程序与进程内存映像的不同之处在于:
a. 可执行程序位于磁盘中而内存映像位于内存;
b. 可执行程序没有堆栈,因为程序被加载到内在中才会分配堆栈;
c. 可执行程序虽然也有未初始化数据段但它并不被存储在位于硬盘中的可执行文件中;
d. 可执行程序是静态的、不变的,而内在映像随着程序的执行是在动态变化的,比如数据段随着程序的执行要存储新的变量值,栈在函数调用时也是不断在变化中。
五、环境变量
和命令行参数argv类似,环境变量表也是一组字符串,如下图:
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明。
由于父进程在调用fork创建子进程时会把自己的环境变量表也复制给子进程,但此后二者的环境变量互不影响,所以a.out打印的环境变量和Shell进程的环境变量是相同的。 按照惯例,环境变量字符串都name=value这样的形式,大多数name由大写字母加下划线组成,一 般把name的部分叫做环境量,value的部分则是环境变量的值。环境变量定义了进程的运行环境,一些比较重要的环境变量的含义如下:
PATH:可执行文件的搜索路径。
echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games
SHELL:当前Shell,它的值通常是/bin/bash。
TERM:当前终端类型
LANG:语言和locale,决定了字符编码以及时间、货币等信息的显示格式。
HOME:当前用户主目录的路径,很多程序需要在主目录下保存配置文件,使得每个用户在运行该程序时都有自己的一套配置。
六、进程状态
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。下面的状态在 fs/proc/array.c 文件里定义:
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
运行状态(running)并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
睡眠状态(sleeping)意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送SIGCONT 信号让进程继续运行。
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用)没有读取到子进程退出的返回代码时就会产生僵死进程。僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
七、进程优先级
进程cpu资源分配就是指进程的优先权(priority)。优先权高的进程有优先执行权利。进程优先权对多任务环境的linux很有用,可以改善系统性能。还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
1、系统进程
无论在linux或者unix系统中,用ps –l命令则会类似输出以下几个内容:
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高。
NI就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值。
PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:
PRI(new)=PRI(old)+nice
这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行。
2、修改进程优先级的命令主要有两个:nice,renice
1>一开始执行程序就指定nice值:nice
nice -n -5 /usr/local/mysql/bin/mysqld_safe &
linux nice 命令详解
功能说明:设置优先权。
语 法:nice [-n <优先等级>][–help][–version][执行指令]
补充说明:nice指令可以改变程序执行的优先权等级。
参 数:-n<优先等级>或-<优先等级>或–adjustment=<优先等级> 设置欲执行的指令的优先权等级。
等级的范围从-20-19,其中-20最高,19最低,只有系统管理者可以设置负数的等级。
–help 在线帮助。
–version 显示版本信息。
2>调整已存在进程的nice:renice
renice -5 -p 5200//#PID为5200的进程nice设为-5
linux renice 命令详解
功能说明:调整优先权。
语 法:renice [优先等级][-g <程序群组名称>…][-p <程序识别码>…][-u <⽤用户名称>…]
补充说明:renice指令可重新调整程序执行的优先权等级。预设是以程序识别码指定程序调整其优先权,您亦可以指定程序群组或用户名称调整优先权等级,并修改所有隶属于该程序群组或用户的程序的优先权。等级范围从-20–19,只有系统管理者可以改变其他用户程序的优先权,也仅有系统管理者可以设置负数等级。
参 数:
-g <程序群组名称> 使用程序群组名称,修改所有隶属于该程序群组的程序的优先权。
-p <程序识别码> 改变该程序的优先权等级,此参数为预设值。
-u <用户名称> 指定用户名称,修改所有隶属于该用户的程序的优先权。
3>也可以用top命令更改已存在进程的nice:
top//#进入top后按“r”–>输入进程PID–>输入nice值
八、进程创建执行
1、起创建进程的事件
在多道程序环境中,只有(作为)进程(时)才能在系统中运行。因此,为使程序能运行,就必须为它创建进程。导致一个进程去创建另一个进程的典型事件,可以有以下四类:
1) 用户登录
在分时系统中,用户在终端键入登录命令后,如果是合法用户,系统将为该终端建立一个进程,并把它插入到就绪队列中。
2)作业调度
在批处理系统中,当作业调度程序按照一定的算法调度到某作业时,便将该作业装入到内存,为它分配必要的资源,并立即为它创建进程,再插入到就绪队列中。
3) 提供服务
当运行中的用户程序提出某种请求后,系统将专门创建一个进程来提供用户所需要的服务,例如,用户程序要求进行文件打印,操作系统将为它创建一个打印进程,这样,不仅可以使打印进程与该用户进程并发执行,而且还便于计算出为完成打印任务所花费的时间。
4) 应用请求
在上述三种情况中,都是由系统内核为它创建一个新进程,而这一类事件则是基于应用进程的需求,由它创建一个新的进程,以便使新进程以并发的运行方式完成特定任务。
2、进程的创建过程
一旦操作系统发现了要求创建新进程的事件后,便调用进程创建原语create()按下述步骤创建一个新进程。
1) 申请空白PCB。为新进程申请获得唯一的数字标识符,并从PCB集合中索取一个空白PCB。
2) 为新进程分配资源。为新进程的程序和数据以及用户栈分配必要的内存空间。显然,此时操作系统必须知道新进程所需要的内存大小。
3) 初始化进程控制块。PCB的初始化包括:
①初始化标识信息,将系统分配的标识符和父进程标识符,填入新的PCB中。
②初始化处理机状态信息,使程序计数器指向程序的入口地址,使栈指针指向栈顶。
③初始化处理机控制信息,将进程的状态设置为就绪状态或静止就绪状态,对于优先级,通常是将它设置为最低优先级,除非用户以显式的方式提出高优先级要求。
4) 将新进程插入就绪队列,如果进程就绪队列能够接纳新进程,便将新进程插入到就绪队列中。
3、进程内存布局分为四个不同的段:
• 文本段,包含程序的源指令。
• 数据段,包含了静态变量。
• 堆,动态内存分区区域。
• 栈,动态增长与收缩的段,保存本地变量。
两种创建进程的方法,fork()和execve()(它们都是系统调用)。
要创建一个子进程可以执行fork()系统调用。然后子进程会得到父进程中数据段,栈段和堆区域的一份拷贝。子进程独立可以修改这些内存段。但是文本段是父进程和子进程共享的内存段,不能被子进程修改。
如果使用execve()创建一个新进程。这个系统调用会销毁所有的内存段去重新创建一个新的内存段。然而,execve()需要一个可执行文件或者脚本作为参数,这和fork()有所不同。
注意,execve()和fork()创建的进程都是运行进程的子进程。
进程执行还有很多其他的内容,比如进程调度,权限许可,资源限制,库链接,内存映射
僵尸进程:一个子进程在其父进程没有调用wait()或waitpid()的情况下退出。这个子进程就是僵尸进程。如果其父进程还存在而一直不调用wait,则该僵尸进程将无法回收,等到其父进程退出后该进程将被init回收。
代码:
1 /**************************************
2 *文件说明:僵尸进程
3 *作者:段晓雪
4 *创建时间:2017年05月16日 星期二 20时52分19秒
5 *开发环境:Kali Linux/g++ v6.3.0
6 ****************************************/
7 #include<stdio.h>
8 #include<unistd.h>
9 #include<stdlib.h>
10
11 int main()
12 {
13 pid_t id = fork();
14 if(id > 0)//parent
15 {
16 printf("the parent is sleeping!\n");
17 printf("pid:%d ppid:%d\n",getpid(),getppid());
18 sleep(10);
19 }
20 else if(id < 0)
21 {
22 perror("parent");
23 exit(-1);
24 }
25 else//child
26 {
27 printf("i am a child!\n");
28 printf("pid:%d ppid:%d\n",getpid(),getppid());
29 }
30 return 0;
31
32 }
孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
代码:
1 /**************************************
2 *文件说明:孤儿进程
3 *作者:段晓雪
4 *创建时间:2017年05月16日 星期二 21时10分50秒
5 *开发环境:Kali Linux/g++ v6.3.0
6 ****************************************/
7 #include<stdio.h>
8 #include<stdlib.h>
9 #include<unistd.h>
10
11 int main()
12 {
13 pid_t id = fork();
14 if(id < 0)
15 {
16 perror("fork");
17 exit(-1);
18 }
19 else if(id > 0)//parent
20 {
21 printf("i am a parent\n");
22 printf("parent:pid:%d ppid:%d\n",getpid(),getppid());
23 sleep(3);
24 exit(-1);
25 }
26 else//child
27 {
28 printf("i am a child\n");
29 while(1){
30 printf("child:pid:%d ppid:%d\n",getpid(),getppid());
31 sleep(1);
32 }
33 }
34 return 0;
35 }
九、进程环境
1、main函数
当内核使用一个exec函数执行C程序时,在调用main函数之前先调用⼀一个特殊的启动例程,可执行程序将此例程指定为程序的起始地址。启动例程从内核获取命令行参数和环境变量,然后为调用main函数做好准备。
2、进程终止
进程终止的方式有8种,前5种为正常终止,后三种为异常终止:
1 从main函数返回;
2 调用exit函数;
3 调用_exit或_Exit;
4 最后一个线程从启动例程返回;
5 最后一个线程调用pthread_exit;
6 调用abort函数;
7 接到一个信号并终止;
8 最后一个线程对取消请求做出响应。
一个进程可以登记若干个个函数,这些函数由exit自动调用,这些函数被称为终止处理函数,atexit函数可以登记这些函数。exit调用终止处理函数的顺序和atexit登记的顺序相反,如果一个函数被多次登记,也会被多次调用。
3、环境表
每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串,环境指针environ是一个全局变量,指向指针数组的地址。通常用getenv和putenv函数来访问特定的环境变量,而不是environ全局变量。如果要查看整个环境,则必须用
environ全局变量。
4、C程序的存储空间布局
正文段:CUP执行的机器指令部分,是共享和只读的。
初始化数据段:又称作数据段,包含了程序中明确需要赋初值的变量。
非初始化数据段:在程序开始执行前,内核将此段中的数据初始化为0或空指针。
栈:自动变量以及每次函数调用时所需保存的数据都存放在此段中。
堆:用于动态存储分配。堆位于栈和非初始化数据段之间。
5、存储器分配:
malloc函数分配指定字节数的存储区,该存储区中的初始值不确定;calloc函数为指定数量且指定长度的对象分配存储空间,该空间中的每一位都初始化为0;
realloc函数更改存储区的长度(增加或减少),新增区域内的初始值不确定,如果ptr为空,realloc和malloc的功能相同。
十、栈帧
1、堆栈
❖ 堆栈是C语言程序运行时必须的一个记录调用路径和参数的空间
➢ 函数调用框架
➢ 传递参数
➢ 保存返回地址
➢ 提供局部变量空间
➢ 等等
❖ 以x86体系结构为例
2、堆栈寄存器和堆栈操作
❖ 堆栈相关的寄存器
➢ esp,堆栈指针(stack pointer)
➢ ebp,基址指针(base pointer)
❖ 堆栈操作
➢ push
栈顶地址减少4个字节(32位)
➢ pop
栈顶地址增加4个字节
❖ ebp在C语言中用作记录当前函数调用基址
3、利用堆栈实现函数调用和返回
❖ 其他关键寄存器
➢ cs : eip:总是指向下一条的指令地址
● 顺序执行:总是指向地址连续的下一条指令
● 跳转/分支:执行这样的指令的时候,cs : eip的值会根据程序需要被修改
● call:将当前cs : eip的值压入栈顶,cs : eip指向被调用函数的入口地址
● ret:从栈顶弹出原来保存在这里的cs : eip的值,放入cs : eip中
● 发生中断时???
4、函数堆栈框架的形成
5、C语言中还使用堆栈进行
➢ 参数的传递
➢ 局部变量的使用
6、堆和栈的关系
1)堆需要用户在程序中显式申请,栈不用,由系统自动完成
2)堆的空间比较大,栈比较小
3)关于生命周期。栈较短,随着函数退出或返回,本函数的栈就完成了使用;堆就要看什么时候释放,生命周期就什么时候结束