【Linux】进程基础

版权声明:本文为博主原创文章,未经博主允许不得转载。Copyright (c) 2018, code farmer from sust. All rights reserved. https://blog.csdn.net/sustzc/article/details/82734146

程序:为了完成特定任务的一系列指令的有序集合.
进程:程序的一次动态执行过程  也称为task  任务   通俗地讲,就是一个正在运行中的程序。
    1、每个进程都有自己的运行状态;
    2、每个进程都有自己的虚拟地址空间;
    3、进程是操作系统分配资源的基本单位;
    4、进程是系统分配CPU、内存、时间片等系统资源的基本单位,同时也是系统分配资源的最小单位。
    
分配资源:运行进程时分配的内存
    
进程和程序的区别:
    1.进程是动态的(加载程序即可),而程序是静态的;
    2.一个进程只能对应一个程序,但是一个程序可以由多个进程组成;
    3.进程是暂存的,而程序是永存的;
    4.进程具有并发性,而程序没有。

        磁盘上的程序被加载到内存上,而进程实际上是执行了磁盘上的程序,
    因此删除一个程序并不会影响正在运行的进程。
    
3G-4G 存放内核  存放PCB 进程控制块

nm 查看进程的运行的地址
    eg:  nm ./main      nm ./test
    
Linux中的页表实际上就是一个数组,页表反映虚拟地址到物理地址之间映射的关系。
内存页 4K或者4M,但是目前大多数的OS都使用4KB大小的页。
在物理内存中称为页帧,其跟内存页大小一致,都是4K。
把虚拟空间的页叫做虚拟页,物理内存中的页叫做物理页,磁盘中的页叫做磁盘页。
运行哪块的代码,就把那里的代码映射到一张内存页中。

    根据程序的局部性原理,当一个程序在运行时,在某个时间段内,它只是频繁地使用一小部分数据,也就是说,
程序的很多数据其实是在一个时间段内都不会用到的,于是人们想到了更小粒度的内存分割和映射的方法,使得程序的
局部性原理得到了更充分的利用,大大地提高了内存的使用率,这种方法就是分页。

    分页的基本方法就是把地址空间人为地等分成固定大小的页,每一页的大小由硬件决定。

两种数据块缓存方式
    数据块缓存  标记这里的缓存是磁盘缓存,下一次访问磁盘的数据时,直接去这里查找即可。
    页缓存:统一缓存数据块和内存页

内核:
    进程管理
    内存管理
    文件管理
    驱动管理
    
用户      指令操作      开发操作  管理操作
                           |                  (用户部分)
用户操作接口   shell外壳  lib   部分指令
                           |
系统调用             系统调用接口
                           |
操作系统(内核)  进程管理 内存管理 文件管理 驱动管理   (系统软件部分)
                           |
驱动程序   网卡驱动    硬盘驱动    其他驱动
                           |
底层硬件     网卡        硬盘        其他   (硬件部分)

系统调用和库函数
    syscall(do_fork)  do_fork实际上是个宏,一个编号
    在内核中查找编号为n的内核代码。
    fork实际上是C库对其的封装,真正在内核中调用的是编号
    系统调用是操作系统提供的API。
    库函数是大牛们对部分的系统调用进行的适度封装,从而形成库,
我们在开发的时候直接调用这些库就可以完成相应的操作。

操作系统如何来进行进程管理呢?
    先描述进程(使用结构体),再组织进程(高效的数据结构)。
    linux中用的是双向循环链表,这是因为进程总是在频繁的创建和销毁,
    这就需要一种插入和删除特别方便的数据结构。
    
解决程序一闪而过(实际上就是进程结束了)
    1.sleep(1);   
    2.system("pause");
    3.getchar(); //等待IO 其实就是让进程再运行一会

存储器分为内存和外存。

内存的作用:
    1.随机访问某一地址空间;
    2.当电源断开后,内存上的东西也就没有了,实际上是一种短暂保存;
    3.容量小。
外存实际上就是磁盘,把内存中的东西保存在磁盘,也就是存入文件中,就可以有效地防止掉电产生的影响。

    几乎所有的操作系统中硬件都使用MMU(内存管理单元,是个硬件设备,集成在CPU中)
来负责虚拟地址到物理地址之间的转换,也可以称为页映射。
    Linux中的页表是三级映射。
    Linux      虚拟内存中的PCB->页目录->页表->物理内存

    页错误:进程所需的某些页不在物理内存中,硬件会捕获到这个异常,产生页错误。然后OS接管进程,重新加载这些页,建立映射关系。
    
    保护也是页映射的目的之一,简单地说,就是每个页可以设置权限属性,谁可以修改,谁可以访问,而只有OS权限可以修改这些属性。

    缺页中断:数据存储在磁盘上,此时代码存储在页表中的标志位是0(ps:此时存储在磁盘中的数据会在标志位为0的右边存储一个地址,
这个地址就是表示存储数据在磁盘中存放的地址,也就是磁盘的起始地址),表示不是在物理内存中存放数据,这就需要把磁盘与物理内存
以及页表和代码连接起来,构成映射关系,然后将标志位改为1(ps:改为1后就变成了物理内存空间)。

页面置换  如何将物理内存的内容置换到磁盘中?如何选择要置换的内容?
    缓存淘汰算法
局部性原理
    本次执行的内容还会再次被执行,也就是说执行过程中,会选择最近一段时间内反复执行的区域去执行。
LRU算法:最近最久没有使用的内容  操作系统会把物理地址中的这部分内容与磁盘进行交换数据。
    LRU算法的设计原则是:如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。
也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。

如何设计一个数据结构用来描述进程?
    1.在N个进程中标识一个唯一的进程(PID);
    2.知道程序执行的代码和数据存储在哪里;
    3.进程都打开了哪些文件;
    4.进程很多,因此进程需要切换(并发);
    5.进程如何退出。
    
    竞争性: 系统数据众多,而CPU资源只有少量,为了高效完成任务,更合理竞争相关资源。
    独立性: 多进程运行期间互不干扰。
    并行:多个进程在多个CPU下分别同时运行。
    并发:多个进程在一个CPU下采用进程切换的方式,使得多个进程得以运行,
        由于CPU切换进程的速度极快,我们无法感受到这种切换,会认为多个进程是在同时运行,
    其实每次只有一个进程在运行。
    
    上下文:每次切换进程的时候,需要保护现场。(相当于中断某个进程后保护现场ebp eip等等,以便于下次执行该进程)
    
进程控制块(PCB)  用来描述和管理进程的运行
    PCB是进程的唯一标识
PCB包含信息:
    1、进程状态(state);
    2、进程标识信息(uid、gid);
    3、定时器(time);
    4、用户可见寄存器、控制状态寄存器、栈指针等(tss)。
    
    代码如何知道在栈中运行?PCB会保存栈顶指针,这样就可以找到栈了。
实际上就是一个结构体task_struct

task_struct内容
    标识符:PID 描述本进程的唯一标识符,用来区别其他进程。  类型为pid_t,其实是一个非负整数。
    状态:任务状态,退出代码、信号等。  R T t D S
    优先级:相对于其他进程的优先级。
    程序计数器:程序中即将执行的下一条指令的地址。(eip寄存器)  用来备份
    内存指针:包含程序代码和进程相关数据的指针。
    上下文数据:进程执行时处理器的寄存器中的数据(休学回来再继续上学)。
    I/O状态信息:包括显示的I/O请求,打开了哪些文件。
    记账信息:可能包括处理器时间总和,使用的时间限制,记账号等,也就是执行多长时间的进程后切换进程。
    描述虚拟地址空间的信息。
    文件描述符表。
    控制终端、Session、进程组信息。
    进程可以使用的资源的上限。
    其他信息。

程序状态字(PSW)  用来指示处理器的状态    

程序:代码       +     数据
      读、执行         读写
进程:代码+数据+堆栈+PCB

    0号进程是写死的,硬编码。
    
    find /usr/src/ -name "*.h" | xargs grep -n struct task_struct {}
    
    cat /proc/sys/kernel/pid_max  查看最多可创建的进程数
    如果是自己创建进程 [2, /proc/sys/kernel/pid_max)
    
ps -ef 与 ps aux区别
    1.-ef是System V风格, aux 是BSD(数据文件)风格
    2.COMMADN列如果过长,aux会截断显示,而-ef不会

    ps -ef  查看进程(可以看到pid 和 ppid)
    ps aux  查看进程(可以看到进程的状态,以及内存的使用率,还有COMMAND)
    
    ps aux | head -n 1  查看进程信息的表头
    
POSIX和UNIX的标准要求"ps -aux"打印用户名为"x"的用户的所有进程,以及打印所有将由-a选项选择的过程。
如果用户名为"x"不存在,ps -aux将会解释为"ps aux",
而且会打印一个警告。这种行为是为了帮助转换旧脚本和习惯。它是脆弱的,即将更改,因此不应依赖。
    
        VSZ 进程占用的虚拟内存大小
        RSS 进程占用的物理内存大小
        STAT 表示进程的状态
        其中S-睡眠,s-表示该进程是会话的先导进程,N-表示进程拥有比普通优先级更低的优先级,
        R-正在运行,D-短期等待,Z-僵尸进程,T-被跟踪或者被停止等等
    df -h 查看磁盘信息
    
UID 用户 PID 进程号 PPID 父进程号  
CMD 该进程对应的可执行程序在哪里 或者说是执行了哪个指令创建的进程

为什么运行hello会有父进程和子进程?
    在bash中会执行./hello这个程序,实际上通过bash创建了一个子进程,
由这个子进程来执行hello这个可执行程序。

创建一个进程的一般工作:
    1. 分配一个PID [0 - /proc/sys/kernel/pid_max]
        0 号进程是内核进程,它创建1号进程(Init进程)。
        0 号进程还将其他进程从物理内存搬到磁盘,和从磁盘搬到物理内存。
        磁盘在交换分区(tmpfs)存放。
    2. 分配PCB, 拷贝父进程PCB的绝大部分数据(代码段、数据段等等信息)
    3. 给子进程分配资源
    4. 复制父进程地址空间
    5. 将子进程置成就绪状态,放入就绪队列。
    
#include <unistd.h>
    pid_t fork( void );
    
返回值:子进程返回0,父进程返回大于0的数(ps:也就是新创建的进程的ID)  失败返回-1
        eg:一个父亲可以有多个儿子,那么就需要标识儿子,因此这里需要返回子进程ID,
        而一个儿子只会有一个父亲,这里返回0就可以了。
    
        父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)
        写时拷贝:一开始拷贝数据的时候并不会为子进程分配内存,只有在修改(或者说写)时才分配内存。
    父子进程共用一块内存空间,这将会省下一部分开销。
    
    写时复制
        两个任务(进程)可以同时自由地读取内存,但任意一个任务试图对内存进行修改时,内存就会复制一份提供给修改方单独
    使用,以免影响到其他的任务使用。
        
        fork创建一个进程,而一个进程就是一个执行流,每个进程都将独立地被执行,
        那么当创建一个进程后,源进程又会创建一个子进程,并且复制源进程中的大量数据(eip,上下文信息),
        同时对子进程中的某些数据进行更新(ps: pid, ppid)
            子进程从源进程返回的位置开始执行自己的进程,当进程结束后,都会向下执行代码,这其实
        是因为两者都保存了eip的值。
        
        fork实际上只产生本任务的镜像,因此须使用exec配合才能够启动别的新任务。产生新进程使用clone函数;
            #include <sched.h>
            int clone(int (*fn)(void *), void *child_stack,
                    int flags, void *arg, ...
                    /* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );
            使用clone可以产生一个新的任务,从指定的位置开始执行,并且共享当前进程的内存空间和文件等,
        如此一来就可以在实际效果上产生一个线程。
        
    特点:
        1.fork一次调用有两个返回值,一个在父进程中返回子进程的pid,一个在子进程中返回0。
        2.父进程和子进程都从fork执行结束之后的位置继续执行。
        3.子进程以父进程为模板进行写时拷贝(PCB、数据和代码)。
        4.创建新进程成功后,系统中出现两个基本完全相同的进程,
    父子进程执行没有固定的先后顺序,哪个进程先被执行取决于系统的进程调度策略(操作系统调度器)。
        
        fork失败
            1.内存分配失败,内存不够;
            2.[0 - /proc/sys/kernel/pid_max],创建进程数目达到了进程id上限;
            3.平台不支持;
            4.系统调用被中断,或者重启。
        总结下实际上就是两点:内存不够 和 创建的进程数目达到了上限。
    
为什么fork一个进程后,ppid会返回1?
    这是因为父进程快速退出,子进程成为孤儿进程,这时候会被1号进程也就是Init进程收养,
并且回收子进程,那么就不会出现僵尸进程。

    fork中有个类似于clone的克隆函数,负责拷贝父进程的内容,(clone创建子进程并从指定位置开始执行)
    有可能父进程执行完了时间片还没到。
    创建新进程成功后,系统中出现两个基本完全相同的进程,
    这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。
    
    fork常规用法:
        父进程复制一个子进程,使得父子进程执行不一样的代码,例如
        1.父进程等待客户端请求,然后创建一个子进程来处理请求。
        2.各个进程执行不同的程序,例如子进程从fork返回后,调用exec函数。
    
    ps -ef | grep a.out | grep -v grep
    搜索没有grep和搜索a.out的进程
    在/proc目录下查找对应的PID,进入该目录后可以cat maps
    查看虚拟内存中的存储
    
kill -l 杀死进程选项列表
kill -SIGSTOP pid 使得该进程进入暂停状态    eg: kill -19 14398 (-19就是-SIGSTOP)
kill -SIGCONT pid 使得该进程恢复之前的进程状态  eg: kill -18 14398 (-18就是-SIGCONT)  continue
    
fork注意点:
    1. fork父子进程交替运行。
    2. 父进程死亡,子进程变成孤儿进程(不是进程状态),由1号进程领养。
    3. 子进程死亡,成为僵尸进程(状态)。
    
    为了将子进程的退出信息反馈给父进程.
    
    当子进程退出并且父进程没有读取到子进程退出的返回码时就会产生僵尸进程。
    僵尸进程会以终止状态保持在进程表中,并且会一直等待父进程读取退出状态码。
    所以,只要子进程退出(子进程遇到return 0或者exit())时,父进程仍在运行,
    并且父进程没有读取子进程的进程状态,那么子进程就会进入僵尸状态。
    父进程中又会保存一个读取子进程退出状态的进程表,没有读取到的话就一直在等待,这样一直会为它分配内存,
    并没有去回收内存,时间长了就会造成内存泄漏。
    只有父进程读取了子进程的退出状态,并且修改了进程表中的状态码,子进程才能正常退出。

    kill 僵尸进程没有用,就相当于杀死一个死人。
    kill 僵尸进程的父进程就可以杀死僵尸进程,这是因为父进程是保存子进程退出状态的,一旦父进程
    没有了,那么退出状态码就没有了,直接销毁掉僵尸进程也是OK的。
——————————————————————————————————————————————————————————————————    
    
中断技术
    分时系统

    
调度
    从就绪态中选择一个进程放到CPU中去运行。

就绪态:创建进程,也就是说等待OS分配CPU以便运行;

运行态:占用CPU正在运行;

阻塞态:等待某个事件的完成。

运行态->消亡

时间片:处于运行中的线程拥有一段可以执行的时间,这段时间就称为时间片。

1.等待态:等待某个事件的完成;     此时线程正在等待某一事件(通常是I/O或者同步)发生,无法执行。
2.就绪态:等待系统分配处理器以便运行;      此时可以立即运行,但是CPU被占用。
3.运行态:占用处理器正在运行。
    运行态→等待态(阻塞态)
        往往是由于等待外设,等待主存等资源分配或等待人工干预而引起的。
    等待态(阻塞态)→就绪态
        则是等待的条件已满足,只需分配到处理器后就能运行。
    运行态→就绪态
        不是由于自身原因,而是由外界原因使运行状态的进程让出处理器,
        这时候就变成就绪态。例如时间片用完,或有更高优先级的进程来抢占处理器等。
    就绪态→运行态
        系统按某种策略(调度策略)选中就绪队列中的一个进程占用处理器,此时就变成了运行态

------------------------------------------------------------
进程状态
/*
* 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 */  磁盘休眠状态,也可称为不可中断睡眠。   (ps:密集读写I/O状态时就会出现)
"T (stopped)", /* 4 */  暂停状态,可以通过发送SIGSTOP信号来暂停进程,通过SIGCONT信号来继续运行该进程。
"t (tracing stop)", /* 8 */  追踪状态,gdb跟踪调试时会用到。
"X (dead)", /* 16 */  死亡状态,这个状态只是一个返回状态,不可见,因为PCB没了。
"Z (zombie)", /* 32 */  僵尸状态
};

后台进程可以输入指令,而前台进程状态下输入命令是不行的。

+表示这是一个前台进程
<表示高优先级
N表示低优先级
L有些页被锁进内存
s包含子进程
l多线程

猜你喜欢

转载自blog.csdn.net/sustzc/article/details/82734146