Linux之进程第一谈

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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整理

本文出自 “暮回” 博客,请务必保留此出处http://muhuizz.blog.51cto.com/11321490/1902097

以上是关于Linux之进程第一谈的主要内容,如果未能解决你的问题,请参考以下文章

宋宝华:谈一谈Linux写时拷贝(COW)的安全漏洞

宋宝华:谈一谈Linux写时拷贝(COW)的安全漏洞

宋宝华:谈一谈Linux写时拷贝(COW)的安全漏洞

Linux-进程描述之进程环境

LINUX PID 1和SYSTEMD PID 0 是内核的一部分,主要用于内进换页,内核初始化的最后一步就是启动 init 进程。这个进程是系统的第一个进程,PID 为 1,又叫超级进程(代码片段

转载腾讯面试题——谈一谈Binder的原理和实现一次拷贝的流程