Linux之进程第一谈

这是一篇迟来的博客因为最近整理的东西有点多,没来得及发,之前写过在Linux下关于进程管理相关的命令,这里开始,重新聊一聊进程。

重提进程概念

首先,怎么理解进程?最简单的话来说,我们写一个简单的C程序,编译链接生成一个可执行文件,这个可执行文件叫做可执行程序,然后我们开始运行它,我们都知道,计算机中,真正具有处理能力的只有CPU,与CPU之间进行数据交换的只有内存,因此,我们要运行该程序,就要将可执行文件加载到内存中,交给CPU执行,我们把加载到内存中正在被执行的程序就叫做一个进程。

我们可以看到,进程实际上是程序的动态运行实例,它能够分配到CPU和内存的资源,担当了系统资源的承担者。

这是我们对进程最基本的一个认识,然后我们开始考虑其他的问题,CPU开始执行该程序,但内存中不可能每次只载入一个程序去运行(要真这样的话,它会慢到让你想拆了它),在这几个G的内存当中,载入很多程序,那么内存怎么来管理他们,CPU又该如何知道我该调用哪个进程呢?

这个就像学校对学生信息的管理一样,学校并不直接和每个学生打交道,但却能知道每个学生的信息,原因就在于学校将所有人的信息都建了一张表,也就是数据库,只要将数据库合理管理,就可以知道每个学生的信息了。对于内存而言,要管理大量的进程信息,它对每个进程都建立了一个结构体,将进程所有需要关注的信息都保存在了这个结构体中,然后又使用了一些较为优的数据结构,将所有的结构体信息串了起来,以后内存的管理工作就是这些数据结构,这大大减少了内存的压力,也方便了下次CPU来运行这些进程时的优先级的问题。有个很重要的结构叫做PCB。这个接下来会提到。

还要多说一点的是,即使某个进程已经加载到了内存中,它也不一定在运行,在内存中受内存调度、程序本身等原因,会处于各种状态。

到这里,我们就可以对进程的概念做一个总结:

进程是操作系统中程序的一个运行实例,它承担了分配系统资源的实体。进程是一种动态描述,因为要加载到内存中去运行,但并不是所有的进程都在运行。当一个程序加载到内存中之后,除了程序数据本身,操作系统还提供了一整套的数据结构来维护这个进程,一个最典型的数据结构就是PCB。对进程的调度管理只需要我们将数据结构有效的组织起来,采用适当的算法。

PCB(Process Control),即进程控制块, 在Linux内核中,PCB是一个叫task_struct的独立结构体,里面包含着关于进程的信息,具体内容这里暂时不讨论,只关注我们常用到的一些:


task_struct 最常见的成员

标识符:即 PID

状态:

优先级:区别权限

程序计数器PC:以PC为代表,在CPU内部各种寄存器,用来表示当前进程执行的状态,一旦程序运行时被切出,就需要保存当前寄存器到PCB(个别需要保存到内核)当中。

内存指针:包括程序代码和进程相关数据的指针,还有其他进程共享的内存块的指针

硬件上下文数据:

I/O状态信息:

记账信息:

关于task_struct,可以在/include/linux/sched.h文件中找到,所有运行在系统里的进程都以task_struct的形式存在内存中。因此,CPU就不在关注每个进程,只需要将所有的PCB组织起来即可。在Linux中,操作系统是通过双链表的形式组织和管理这些结构体的,每个结点还可能被置于其他的结构当中。对调度器而言,当我们知道了各个进程优先级之后,需要对各个进程的PCB进行组织,实现快速找到目标结点,例如:哈希、最大堆和最小堆。这些算法都依赖与数据结构的支撑。

进程信息可以通过/proc系统文件夹查看,要获取PID为 *** 的进程信息,需要查看/proc/*** 这个文件夹,可以通过top或ps等命令查询。前面提到过,这里就不再多说。

进程标识符

为了区分每个进程,我们可以通过进程标识符来做到,就像每个人的ID CARD一样,确定身份的唯一标识。在Linux下,pid是一个无符号长整形的数据。

进程id(PID)

父进程id(PPID)

在Linux下获得pid和ppid的方法如下:

#include <sys/types.h>
#include <unistd.h>
    printf("%d",getpid());
    printf("%d",getppid());

进程位置

当程序执行时,操作系统将可执行程序复制到内存中,程序转化为进程有以下几个步骤:

1、内核将程序读入内存,前提:程序分配内存空间

2、内核为进程保存PID及相应的状态信息,把进程放到运行队列中等待执行。程序转化为进程后就可被操作系统的调度程序执行。

程序转化为进程,除了PCB外还有其他的数据结构,例如进程地址空间结构体(mem_struct),这个后面再解释。

 进程的内存映像:即进程地址空间,是指内核在内存中如何存放可执行程序文件。在将程序转化为进程的过程中,操作系统将可执行程序从硬盘复制到内存中,布局如下:

这里有几点需要我们注意:

1、程序的地址空间是虚拟内存,不是物理内存

2、从低地址到高地址依次是:正文(代码段),只读静态数据区(已从初始化的全部变量,未初始化的全局变量),堆区,共享区,栈区。其中,堆栈相对而生

3、每一个进程都有一个地址空间,是在内存中真实存在的一块空间。

4、堆栈段向上还有区域:命令行参数和环境变量

了解了这些内容之后,这里我们在Linux环境下,对地址空间内容做一简单验证。

测试代码:

// mymem.c
#include <stdio.h>
#include <stdlib.h>

int g_val = 10;
int g_val1;
int main(int argc, char* argv[],char* env[])
{
    int* mem1 = (int*)malloc(4);
    int* mem2 = (int*)malloc(4);
    int* mem3 = (int*)malloc(4);
    int* mem4 = (int*)malloc(4);
    const char* msg = "hello world";
    int a;
    int b;
    int c;
    printf("code : %p\n", main);
    printf("read only : %p\n",msg);
    printf("g init value: %p\n",&g_val);
    printf("uninit value: %p\n",&g_val1);
    printf("heap1: %p\n",mem1);
    printf("heap2: %p\n",mem2);
    printf("heap3: %p\n",mem3);
    printf("heap4: %p\n",mem4);
    printf("stack: %p\n", &msg);
    printf("stack: %p\n", &a);
    printf("stack: %p\n", &b);
    printf("stack: %p\n", &c);
    printf("argc与argv####################################\n");
    printf("argc addr : %p\n",&argc);
    int i = 0;
    for(; i< argc;i++)
    {
        printf("%d -> %s --> %p\n", i, argv[i] );
    }
    printf("环境变量####################################\n");
    for(i=0;env[i] != NULL;i++)
    {
        printf("%d -> %s --> %p\n", i, env[i]);
    }
    return 0;
}

观察结果我们可以发现,除了从正文到堆区之间的地址顺序符合我们的预期,还有一些其他值得我关注的内容。

我们没有设置任何的环境变量,依旧打印出了一堆环境变量,这里简单讨论main函数中环境变量的来源,是由于shell在执行某个进程时,并不是自己去执行,而是派生一个子进程去执行,在这里的这些环境变量都是继承自当前shell。

我们可以这样理解,凡是在shell下运行的程序,都有着一份环境变量。这些环境变量继承自shell,在shell下运行的进程都可以看到这份环境变量,那么就可以通过这些环境变量修改自己的一些行为,这就是配置环境变量之后,让操作系统体现出不同行为的一个原因。

进程和程序的区别

首先来理解一个概念叫做进程映像。

    进程映像:把程序加载到内存中时,操作系统创建的一系列的数据结构,统一叫做进程映像。

进程映像的位置依赖于使用的内存管理方案。

可执行程序没有堆栈,因为程序只有加载到内存中才会分配堆栈(堆栈是一个运行时概念)。

可执行程序虽然也有未初始化数据段,但它并不被存储在位于硬盘中的可执行文件。在Linux下  size a.out  命令可以查看程序的组成。

这里解释data段和bss段(better save spaces)。已初始化的全局变量保存在data段,未初始化的全局变量保存在bss段。这里所谓bss段的节省空间是针对硬盘(存储器)而言的,在硬盘中,未初始化的全局变量在硬盘中只需要记录其类型即可,当加载到内存中之后,会默认给随机值(或初始化为0),也就是说,在内存中,已初始化和未初始化的全局变量所栈空间大小一致。

  主要区别:

可执行程序是静态的,内存映像是随程序执行动态变化的。

进程是可以被调度器调度的,并且可以在内存中运行,而且可以被切换,拥有自己的硬件上下文。

进程拥有运行时堆栈,而程序没有。

进程状态

"R (running)", /* 0 */     运行

"S (sleeping)", /* 1 */    可中断睡眠,可外接唤醒

"D (disk sleep)", /* 2 */  深度睡眠,不可中断,只有自己醒来

"T (stopped)", /* 4 */     停止
"t (tracing stop)", /* 8 */

"X (dead)", /* 16 */        死亡

"Z (zombie)", /* 32 */      僵尸状态

D状态是操作系统为了照顾硬件而设定的,通常异味着IO压力大或系统遇重大问题,通常运维人员遇到;

t状态暂不谈论;

Z(僵死状态):僵尸进程。我们可以这样理解,子进程是父进程派生出来执行某一项任务的,父进程并不关心子进程死活,只关心的是子进程是否将任务完成(通过退出码判断)。子进程退出之后,父进程或关心它的进程需要读取到它的退出码来判断该进程是否完成工作,如果子进程退出立即释放PCB,导致无法获得退出码,从而导致僵尸进程.僵尸进程不回收,会造成内存泄漏问题.

R状态是最容易理解的状态,但需要知道的是,即使某个进程处于R状态,那么此时,它也不一定正在CPU上运行。因为CPU会将所有的处于R状态的PCB组织起来,供操作系统调度。某一时刻只有一个进程在CPU上运行,其他running状态的PCB都在运行队列当中。

通过kill命令可以改变进程状态,具体操作参照

http://muhuizz.blog.51cto.com/11321490/1896983

进程基本概念就说到这里,下一篇,主要是关于进程控制的内容,关于本文中用到的代码,可以在下方地址下载:

https://github.com/muhuizz/Linux/blob/master/Linux%E7%B3%BB%E7%BB%9F%E7%BC%96%E7%A8%8B/%E8%BF%9B%E7%A8%8B/code/mymem.c

-----muhuizz整理

时间: 02-26

Linux之进程第一谈的相关文章

.NET跨平台实践:再谈用C#开发Linux守护进程

Linux守护进程是Linux的后台服务进程,相当于Windows服务,对于为Linux开发服务程序的朋友来说,Linux守护进程相关技术是必不可少的,因为这个技术不仅仅是为了开发守护进程,还可以拓展到多子进程,父子进程,父子进程通讯与控制等方面,是实现Linux大型服务的基础技术之一. 去年我也曾写了一篇关于守护进程的帖子,名字叫<.NET跨平台实践:用C#开发Linux守护进程>,这篇文章的的确确实现了一个Daemon,不过,它有一个弱点,不能运行多线程! 这篇帖子的目的就是进一步完善,让

Linux之进程管理(1)基本介绍

Linux之进程管理(1)基本介绍 什么是进程: linux系统中,进程管理相当重要.所谓进程,就是相当于触发任何一个事件时,系统都会将此事件当成一个角色定义成为一个进程,并且给予这个进程一个IP,成为PID,同时依据启发这个进程的用户与相关属性关系,给予这个PID一组有效的权限设定.然后这个PID能够在linux上面进行的各种动作,这个PID代表一个角色. PID的产生: 在linux中执行此程序文件中,内核并不是马上进行执行,而是根据程序文件的执行者的选项和一些相应的属性等参数,然后包括其中

转:linux守护进程的启动方法

Linux 守护进程的启动方法 作者: 阮一峰 日期: 2016年2月28日 "守护进程"(daemon)就是一直在后台运行的进程(daemon). 本文介绍如何将一个 Web 应用,启动为守护进程. 一.问题的由来 Web应用写好后,下一件事就是启动,让它一直在后台运行. 这并不容易.举例来说,下面是一个最简单的Node应用server.js,只有6行. var http = require('http'); http.createServer(function(req, res)

Linux之进程管理及Shell脚本

使用!来调用过往命令 !! 重复执行上一条指令 !a 重复执行上一条以a为首的指令 !nubmer 重复执行上一条在history表中记录号码为number的指令 !-number重复执行前第number条指令 ailas abc='cd xxxxxxxxxxx/xxxxxxxx/xxxxxxxxxxxxx' unalias abc " "将一串字符当成字符串来看,可以转译特殊字符 ' '将一串字符当成字符串来看,无法转译特殊字符 ·能够返回命令执行的结果 echo `uname -a

Linux守护进程的编程实现

Linux 守护进程的编程方法 守护进程(Daemon)是执行在后台的一种特殊进程.它独立于控制终端而且周期性地执行某种任务或等待处理某些发生的事件.守护进程是一种非常实用的进程.Linux的大多数server就是用守护进程实现的.比方,Internetserverinetd,Webserverhttpd等.同一时候,守护进程完毕很多系统任务.比方,作业规划进程crond,打印进程lpd等. 守护进程的编程本身并不复杂,复杂的是各种版本号的Unix的实现机制不尽同样,造成不同Unix环境下守护进

Linux/UNIX进程间的通信(1)

进程间的通信(1) 进程间的通信IPC(InterProcessCommunication )主要有以下不同形式: 半双工管道和FIFO:全双工管道和命名全双工管道:消息队列,信号量和共享存储:套接字和STREAMS 管道 pipe函数 当从一个进程连接到另一个进程时,我们使用术语管道.我们通常是把一个进程的输出通过管道连接到另一个进程的输入. 管道是由调用pipe函数创建的: #include<unistd.h> int pipe(intpipefd[2]); 经由参数pipefd返回两个文

笔记整理--Linux守护进程

Linux多进程开发(三)进程创建之守护进程的学习 - _Liang_Happy_Life__Dream - 51CTO技术博客 - Google Chrome (2013/10/11 16:48:27) Linux多进程开发(三)进程创建之守护进程的学习 2013-07-04 17:25:35 标签:守护进程 daemon Linux多进程开发 系统编程 原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.http://liam2199.bl

Linux之进程管理(2)相关命令之四

Linux之进程管理(2)相关命令之四 设置或调整进程优先级命令: nice  rnice nice 命令 nice - run a program with modified scheduling priority 运行一个程序时修改调度其进程优先级 格式及用法: nice  [options]  [command [args]] -n  # :#表示要设置此程序nice值,-20到19,值越下,优先级越高 注:-20到19的每个nice值分别对应(Centos5中为100-139,CentO

Linux之进程管理(3)作业管理

Linux之进程管理(3)作业管理 Linux的作业控制介绍: 前台作业:通过终端启动,且启动后一直占据终端: 后台作业:可通过终端启动,但启动后转入后台运行(释放终端): 让进程作业运行在后台: 1.对运行中的进程:使用Ctrl+z 2.尚未启动的作业:COMMAND &  (在命令行的最后面加一个&符号) 后台作业与终端关系的处理: 后台作业虽然被送往后台允许,但其依然与终端相关:退出终端,将关闭后台作业.如果希望送往后台后,同时剥离与终端的关系.可以使用下面两种方法: 1.# noh