美文网首页
进程线程相关

进程线程相关

作者: 植物大战代码 | 来源:发表于2020-07-28 17:42 被阅读0次

    1.介绍一下进程和线程

    应该从调度,并发性,拥有资源,系统开销四个方面比较
    主要介绍区别:
    进程是操作系统分配资源的基本单位,比如打印机、缓冲队列这些都记录在进程控制块PCB中,
    从更微观上来看,线程是操作系统进行CPU调度的基本单位,只由相关堆栈、寄存器和线程控制表TCB组成,这个调度既可以由用户自己控制并发执行,也可以由操作系统内核来控制
    一个进程中可以有多个线程在运作实现进程所表现出来的功能,进程拥有一个完整的虚拟地址空间,不同的进程拥有不同的虚拟地址空间,且他们之间有操作系统提供的内存保护不能够互相寻址;同一个进程中的线程共享所在进程的同一地址空间
    虽然进程和线程都可以并发但是进程创建和切换所带来的空时开销比线程用的多得多,进程切换需要涉及当前进程CPU环境和一些设置的保存而线程值需要保存少量寄存器的内容,而且因为多线程可共享同一进程的空间,所以线程间的通信也比进程间通信要简单得多,之所以引进线程的概念正是希望减少处理机的空转和调度切换的时间提高系统的执行效率。

    进程状态的具体表现是由CPU的寄存器确定的:程序计数器(PC),堆栈指针(SP),通用寄存器以及 MMU(Memory ManagementUinit)页表。这些寄存器加上内存中的内容,磁盘文件,以及其他外设。所有这些告诉来我们一台计算机的一切。当进程需要切换时,需要先保存所有的CPU寄存器到切换出CPU的进程的进程结构中,然后用切换进CPU的进程的进程结构中保存的CPU的寄存器的值回复CPU状态。线程切换上下文切换的原理与此类似,只是 线程在同一地址空间中,不需要MMU等切换,只需要切换必要的CPU寄存器,因此,线程切换比进程切换快的多。

    那么子进程和线程有什么区别?
    子线程是父进程的一个副本,而且从父进程那里继承几乎所有的资源然后独立作为一个线程运行,各自在自己的空间;但是线程只拥有自己的堆栈,和他的创建者进程还有其他线程共享全局变量、文件描述符、信号等资源

    (线程的实现、线程的分类)既然线程调度既可以由用户自己控制并发执行,也可以由操作系统内核来控制,详细说说?

    1.用户级线程:
    应用程序通过线程库设计成多线程程序,通过调用线程库的相关方法创造在同一个进程中运行的线程。
    这个可以再用户空间执行的线程库是操作系统提供的,有线程创建、调度、撤销、线程间通信、存储线程上下文等功能。用户级线程的调度算法和调度过程全部由用户自己选择和确定,与内核无关,此时操作系统内核的调度单位是进程, 线程调度算法在进程的调度空间内设置;用户级线程的调度算法只进行线程上下文的切换,不进行处理机切换,过程中内核不参与,只在用户栈和用户寄存器等空间进行
    优势在于:
    一个是上下文切换的时间和空间开销小,管理线程的数据结构都在同一进程的用户地址空间,线程切换不必到核心态去进行,再者就是不需要操作系统内核的特殊支持,移植性好,不需要对底层的内核进行修改,只需要能有这个线程库即可
    劣势在于
    调度设置在用户层次,如果内核仅支持单线程,,如果一个线程阻塞可能导致同一进程内的其他线程也阻塞

    2.内核级线程
    内核级线程情况下是,应用程序没有线程管理的代码,只有一个到内核级线程的编程接口API,由内核来维护进程和线程的上下文信息,调度也在内核内部架构上完成

    多处理器系统中,一个内核级线程阻塞,核心可以调度同一进程内的另一个线程

    3.包括Linux在内的操作系统都可以提供这两种线程的组合方式
    就是,线程在用户空间创建,调度和同步部分在应用程序中进行,但是可以吧部分程序映射到一些内核级线程,由操作系统调度
    多对一模型:许多用户级线程映射到一个操作系统内核线程,而且不支持内核级线程的操作系统所实现的用户级线程库也是这种多对一方法
    一对一:更好并发性,一个阻塞另一个还能接着运行,缺点就是一对一创建内核线程的巨大开销
    多对多: 内核线程数可以比用户线程数要少,但是要注意线程的数量控制系统性能


    2.线程的基本状态和状态之间的关系

    线程有三个基本状态:执行、就绪和阻塞
    线程没有进程的挂起状态,因为他只是跟内存和寄存器有关的概念,不会因为交换而进入外存

    5种基本操作来转换线程的状态:
    1.派生:线程从进程内派生出来,可以由进程也可以由线程派生,用调用相应的库函数实现
    具有相应的数据结构指针和变量,他们作为寄存器上下文放在寄存器和堆栈中
    新派生的线程被放在就绪队列里,为就绪状态
    2.阻塞:等待某事件发生
    3.激活:阻塞的线程等待到事件发生以后,变为就绪状态进入就绪队列
    4.调度:就绪态进程被调度就进入执行状态
    5.结束:执行完毕则进行结束操作,线程的寄存器上下文和堆栈内容会被释放。

    同进程内的所有线程必须同步!因为他们共享所在进程的资源和地址空间,系统必须为线程执行提供同步控制机制,这个同步机制和进程中的同步控制机制相同(信号量!)


    3.进程的基本状态和状态之间的关系

    进程三个基本状态:运行、就绪和阻塞
    阻塞:等待某种事件的完成,如IO操作,申请缓存空间,进程同步等,暂时不能运行的状态 在阻塞队列
    就绪:已经获得处理处理机以外的所有资源 在就绪队列

    还有其余两个状态可以更好地描述进程的生命周期:
    新建状态:进程被创建以后在等待系统分配好资源的过程
    终止状态:进程正常或异常结束,被操作系统从运行态释放,在终止状态停留完成剩余工作,比如操作系统需要把他的一些数据移交给其他进程,这时他已经不再被调度了
    挂起状态,也叫静止状态:在多个进程处于不处于执行状态同时占据内存而使得处理机闲置,造成资源的浪费,于是将这些进程的程序、除了PCB以外的数据移交到外存,释放出足够的内存空间给可以执行的进程,系统的suspend()原语实现此操作,

    进程被创建以后,等待操作系统做好相关的准备工作(比如进程id关联进程,并创建进程控制块PCB)以后,加入就绪队列,这是处理机调度选中了一个进程,让他占用处理机开始运行,进入运行态,对于运行状态的进程如果时间片轮转完了就会重新恢复就绪态等待下一轮,进程在运行过程中如果遇到一些需要等待的情况(比如等待系统服务,IO,等待同步信号之类的),就会自己阻塞自己,进入阻塞状态,加入阻塞队列,在某一时刻别的进程或者是操作系统唤醒了这个阻塞的进程(比如进程的等待得到满足,等待的时间到达),就会将该进程的PCB插入就绪队列,等待处理机调度,进程正常或异常结束,被操作系统从运行态释放,在终止状态完成程序工作


    4.进程PCB和线程PCB大概分别有哪些内容,说说看?

    进程PCB进程控制块: 进程id,进程状态,现场信息(比如寄存器,计数器等),优先级,程序地址,同步机制,资源清单,数据结构的链接信息等等
    线程TCB线程控制块:线程id,线程状态,现场信息,调度参数比如优先级等
    他们有什么区别?
    线程控制块是进程控制块的附属和组成


    5.进程间的同步机制(操作系统级)?

    进程间的同步是由操作系统来完成的,
    因为线程又是“轻量级进程”,有两种实现方式,
    1.临界资源
    一次仅允许一个进程使用的资源叫做临界资源,必须互斥访问,访问的那段代码叫做临界区
    临界资源的访问分位四个部分:进入区(检查条件,设置标志),临界区(访问临界资源),退出区(清楚标志),剩余区(其他)

    2.互斥、同步
    临界区互斥的底层方法
    1.软件实现:设置标志 皮尔森算法
    2.硬件实现:屏蔽中断 硬件逻辑实现资源标志算法

    用信号量机制实现互斥同步
    wait()申请资源和signal()释放资源,或者P-V操作 ,这是两个原语,具有原子性
    信号量只能被wait()和signal()访问
    互斥:信号量初始为1,wait()和signal()操作夹紧访问互斥资源的那个行为
    同步:信号量初始为0,某进程提供某个资源,在这个行为后面signal()一下;某进程用到某个资源,在这个行为前面wait()一下

    不同的信号量类型
    1.整型信号量 定义为表示该资源数目的整型量 ,“忙等”现象
    2.记录型信号量 一个变量value表示资源数目,还有一个链表,连接所有等待该资源的进程
    没有资源就把进程插入链表,资源释放了就将第一个等待的资源唤醒

    3.关于管程
    管程是一个语言成分,他的互斥完全有编译程序在编译的时候自己添加
    管程实质上是一个抽象类,抽象了系统的硬件和软件资源的表现形式,有成员变量和函数,比如进程要用一个打印机,管程就会初始化自身让他表示成一台打印机的模样,然后这个进程进入管程后利用管程的函数或者说是方法对打印机进行操作,进程以为自己在造作实际的打印机但是他操作的只是管程,管程通过每次只让一个进程进入来实现进程同步

    4.经典的同步问题
    生产者消费者问题

    # -*- coding: utf-8 -*-
    from threading import Thread
    import time
    
    queue = []
    
    
    class Producer(Thread):
        def __init__(self, name):
            Thread.__init__(self)
            self.name = name
    
        def run(self):
            while 1:
                queue.append(1)
                print("Producer: %s create a product" % self.name)
                print("Producer: %s put a product into queue" % self.name)
                time.sleep(0)
                if len(queue) > 20:
                    print("queue is full!")
                    time.sleep(1)
    
    
    class Consumer(Thread):
        def __init__(self, name):
            Thread.__init__(self)
            self.name = name
    
        def run(self):
            while 1:
                try:
                    queue.pop()
                    print("Consumer: %s get a product" % self.name)
                    time.sleep(2)
                except:
                    print("Queue is empty!")
                    time.sleep(2)
                    print("Consumer: %s sleep 2 seconds" % self.name)
    
    
    def test():
        p1 = Producer("Producer-1")
        c1 = Consumer("Consumer-1")
        c2 = Consumer("consumer-2")
    
        p1.start()
        c1.start()
        c2.start()
    
    
    if __name__ == "__main__":
        test()
    

    读者-写者问题


    6.进程的调度算法有哪些(操作系统级)?

    进程的调度算法由操作系统内核来完成
    因为线程是“轻量级的线程”,在Linux中,线程是由进程来实现,线程就是轻量级进程,好处的之一是:线程调度直接使用进程调度就可以了,没必要再搞一个进程内的线程调度器
    1.先来先服务FCFS:按进程变为就绪状态的先后次序分派处理机,非抢占式,有利于长作业不利于短,有利于处理机繁忙不利于IO繁忙的
    2.短作业优先:估计运行的时间最短的进程先分派处理机,保证了资源的充分利用,但是会造成长作业等待
    3.时间片轮转:分时系统,剥夺式进程调度,10-100ms数量级,
    4.优先级算法:每个进程一个调度优先级,然后找到最高优先级的进程。分位静态优先级和动态优先级,一般根据占用处理机时间时间长短或者等待时间长短动态设定优先级,
    5.多级反馈算法:综合时间片轮转和优先级算法的剥夺式进程调度。有多个就绪队列,分别根据进程运行的情况赋予进程不同的优先级,可以把1的优先级最高,逐级降低,时间片长度与优先级成反比。新进程进入内存以后,先放入队列1按先来先服务的算法调度,如果所在时间片内未运行完,就投入下一个优先级更低的队列末位,依次类推,知道在可运行完的那个队列按时间片轮转进行。优先级高的队列为空时才执行低优先级的进程。他也会造成饥饿问题,对最低就绪队列中等待了很长时间的进程的优先级重新设置。

    什么时候进行进程调度?
    1.进程主动放弃处理机:比如进程运行完毕;比如进程状态转换,如阻塞;比如时间片轮转完一轮;比如进程执行完系统调用回到用户态;
    2.处理机可剥夺抢占时,可能优先级高的进程会抢占处理机


    进程间通信的方法有哪些?

    进程之间需要协作就需要通信,通信传递的信息主要由两类,控制信息和数据信息;控制信息主要是通过信号方式,控制信息传输主要是信号方式;传输数据信息的话有管道,消息队列,信号量,共享存储区机制等等,还有通过进程占用不同端口号进行网络传输
    一是信号机制
    又叫软中断,unix软终断就是像进程中的p-sig那个变量送入一个0-19之间的整数,系统v提供了19种软中断信号,比如说终端挂断没退出,系统调用错误,进程终止等等,进程之间可以利用它发送少量信号并适当处理

    二是管道
    管道又称为“两个进程打开的文件”,在物理上由文件系统的高速缓存区组成,允许两个进程间按照先进先出的方式传输数据,一个进程写另外一个读出来,系统负责他们之间的同步执行,只能够单向传递消息,我们需要像文件一样open(),write(),read()来使用它
    管道分为匿名管道和命名管道。匿名管道用于密切相关的父子进程或者兄弟进程,有名管道用于一般的无家族关系的进程间通信

    剩下的消息队列、信号量、共享存储都是用索引表+实例表这样的存储结构来描述的

    三是消息队列
    直接通信模式下,接收者进程用消息队列的方式来管理各发送方进程发来的消息,通常发送方在自己的工作区中按消息的数据格式形成一个消息,然后在公共存储区申请一个消息缓冲区,把消息放到消息缓冲区中,链表链入到接收者进程的消息队列中,消息队列需要解决同步互斥问题和阻塞问题。
    类似于一个信箱机制,发送进程吧消息发送到某一个中间实体,就是消息队列,然后接受进程从这个消息队列上取出消息,就是队列的入队和出队

    四是信号量
    信号量机制被用来解决进程间的互斥同步问题,主要是控制多进程对临界资源的访问,还有例如生产者-消费者这种模型的同步问题,在并发编程中起到作用。

    五是共享存储区
    这是进程最快捷有效的通信手段,由进程之间访问某些共享的虚拟存储空间实现通信,系统管理一组共享主存控制块,进程提出申请,系统分配返回一个共享主存段的标识号,然后共享段附加申请通信的进程的虚拟地址空间来使用


    7.在使用多线程的时候要注意哪些问题?

    1.线程安全性问题
    一个常见的问题就是竞态条件,因为多个线程会共享同一个进程中的内存地址空间并且并发执行,他可能会访问或者修改其他线程正在使用的变量,这样多线程运行时候会出现无法预料的数据变化而发生错误
    如果不能保证写入过程的原子性,会出现脏读脏写的情况,即线程不安全。Python的GIL只能保证原子操作的线程安全,因此在多线程编程时我们可通过加锁来保证线程安全
    哪些类一定是线程安全的:类不包含任何域,也不包含任何对其他类中域的访问,这样的类运行在两个线程上时候会像是两个单独的实例,没有共享状态
    要使多线程的行为可预测,就要必须对共享变量进行协同,这就涉及到线程之间的同步机制了,比如Java可以把方法设置为synchronized
    线程间的同步机制有哪些?又一个问题

    2.线程活跃性问题
    比如A在等待B释放某一个资源,但是B一直都没有释放,A就会一直等待下去,常见问题概括为 死锁,饥饿,活锁
    死锁:多个进程因为争夺某资源而造成的一种僵局,他们都在等待别人释放但同时自己也拿着别人需要的资源不放,所有进程都无法推进的状态
    活锁:是指线程1可以使用资源,但它很礼貌,让其他线程先使用资源,线程2也可以使用资源,但它很绅士,也让其他线程先使用资源。这样你让我,我让你,最后两个线程都无法使用资源。
    饥饿:就比如在抢占式的线程优先级调度中,优先级低的会永远被比他高的线程抢占CPU,造成有些线程长时间处于阻塞状态,这就是线程饥饿

    3.性能问题
    包括服务时间过长,响应不灵敏,吞吐率过低,资源消耗过高等等
    这就涉及到线程的上下文切换带来的开销,而这种开销可能是不必要,在他影响性能的时候更是要考虑线程切换的问题改进
    这就是为什么不能一味的开线程解决并发问题
    线程的上下文切换?又一个问题
    当CPU从执行一个线程切换到执行另外一个线程的时候,它需要先存储当前线程的本地的数据,程序指针等,然后载入另一个线程的本地数据,程序指针等,最后才开始执行。这种切换称为“上下文切换”(“contextswitch”)。CPU会在一个上下文中执行一个线程,然后切换到另外一个上下文中执行另外一个线程。如果没有必要,应该减少上下文切换的发生。


    8.进程池和线程池

    进程池或者线程池都是指用来优化、简化程序内部进程或者线程使用的软件管理器,池子里有一个待执行任务和已完成任务的内部队列,以及一些执行这些任务的线程或者进程。池子主要是可以实现复用,当一个线程或者进程在生命周期以内可以被多次用于执行不同的任务,用池子里已有的进程或者线程进行复用,避免重新创建带来的开销


    9.僵尸进程和孤儿进程

    Linux里面,除了0#那个进程,每个进程都要有一个父进程,每个进程的PCB结构都记录有子进程和家族关系,进程按照这样一个继承关系形成进程树

    僵尸进程
    子进程终止或者退出的时候系统内核将向其父进程发sigchld信号,如果父进程没有处理该信号,子进程就会变成僵尸进程,父进程应当使用wait(),waitpid()等进行等待,取得子进程的退出码,获取子进程的状态

    孤儿进程
    父进程退出以后,子进程还在执行的进程。在进程树里面,如果一个有子进程的进程死掉了,他的子树就在这棵进程树上断开了,成了孤儿进程,Linux需要维持完整的进程树,系统会规定1#进程init来领养所有的孤儿进程,所以孤儿进程会成为init的子进程正常的终止


    10.死锁,活锁,饥饿的具体?怎么解决?

    死锁:

    死锁指的是多个进程因为争夺某资源而造成的一种僵局,他们都在等待别人释放但同时自己也拿着别人需要的资源不放,所有进程都无法推进的状态
    必须有四个条件:互斥(争夺的这个资源一次只能被一个线程占有),不剥夺(资源不能被抢占),请求并保持(进程请求其他资源的同时对自己占有的资源保持不放),循环等待(有这样一个进程的循环等待链存在,他们之间是这样一个请求并保持的关系)
    针对这四个条件可以提前采取措施预防死锁的发生:对于互斥可以

    活锁:

    饥饿


    11.多线程的各种锁

    https://blog.csdn.net/u010953880/article/details/87934244


    12.python多线程

    1.start(),join()

    线程的顺序执行还是多线程并发,取决于join函数的位置。join函数的作用是等待当前线程结束,所以每一个线程创建之后,调用start函数,这是在后面跟上该线程的join函数,那么就是顺序执行,如果多个线程先完成创建和start,最后加上join函数,那么就变成了多线程并发。

    2.Python的锁机制

    Python 的 threading 模块引入了锁(Lock)。
    threading 模块提供了 Lock 和 RLock两个类,它们都提供了如下两个方法来加锁和释放锁:
    acquire(blocking=True, timeout=-1):请求对 Lock 或 RLock 加锁,其中 timeout 参数指定加锁多少秒。
    release():释放锁

    获取锁和释放锁的语句可以用Python的with来实现
    Lock是一次性锁,Rlock是嵌套型锁,他可以对使用了锁的方法再加锁

    条件变量,线程在执行时,当满足了特定的条件后,才可以访问相关的数
    事件:通过event的set(),和clear()

    可以用with语句的上下文管理器,来代替锁或者信号量的获取和释放

    3.用队列实现线程通信


    相关文章

      网友评论

          本文标题:进程线程相关

          本文链接:https://www.haomeiwen.com/subject/sycqrktx.html