linux中的进程与线程那点事儿

文章目录

  • 进程与线程
    • 1. 进程
    • 2. 线程
    • 3. linux中进程和线程的区别
    • 4. linux系统与用户程序的关系
    • 5. linux系统创建进程的方式

进程与线程

a) 进程和线程是操作系统的概念
b) linux系统属于分时操作系统,可处理并发任务同时保证快速响应,采用时间片轮转调度机制,即
操作系统将cpu时间划分为时间片,每个任务只占用一个时间片时间,然后调度队列中的下一个任务执行

1. 进程

  1. 程序: 文本,描述数据的组织结构和对其进行的操作,存放在磁盘中
  2. 进程: 运行中的程序,操作系统将其从磁盘加载进内存,再加载进寄存器
    为其分配资源,包括cpu时间,内存,I/O设备
  3. 描述进程的逻辑内容:
  • 可执行代码(规则)
  • 输入输出数据(main函数的输入输出)
  • 文件描述符(I/O资源)
  • 调用堆栈(函数的调用关系)
  • 堆栈(保存运行过程中产生的数据,堆由程序员管理,栈由操作系统管理)
  • 进程控制块PCB(包含进程的描述信息和控制信息)
    包含进程id, 进程状态, 进程切换时(中断/异常后进入中断/异常处理程序,也是进程切换)
    保存的寄存器状态, 虚拟地址空间和物理地址的对应关系, 打开它的终端的信息,
    当前工作目录, umask掩码, 文件描述符, 信号相关的信息, 用户id和组id, 会话和进程组,
    可使用的资源上限($ ulimit -a查看当前shell所能使用的资源)
附: c++内存模型:                                                            
- 栈(编译器管理, 局部变量,函数参数,返回值) 
- 堆(程序员管理)                                                      
- 静态区(操作系统管理, 全局变量,静态变量)                             
- 文字常量区(操作系统管理, 常量字符串)                                
- 程序代码区(内存中) 
  1. 不同进程的内容完全隔离,不共享

  2. 进程间通信提供了进程间数据共享的途径,开销都比较大(类比深拷贝)

     进程间通信的目的:                                                      
     + 数据传输: 一个进程将数据发送给另一个进程                              
     + 资源共享: 多个进程共享同样的资源,需要内核提供互斥和同步机制           
     + 通知事件: 一个进程向别的进程发送消息,通知他们发生了某种事件           
     + 进程控制: 一个进程完全控制另一个进程的执行(如DEBUG进程),控制进程希望能够拦截
     			另一个进程的所有陷入和异常及其状态改变
                            
     通信方式包括:                                                            
     + 信号(用于软中断和异常处理)                                            
     + 信号量(用于进程同步,避免数据竞争)                                     
     + 管道                                                                  
     + 消息队列                                                              
     + 共享内存                                                              
     + socket(包括网络套接字和unix域套接字)                                  
    

    进程的状态:
    阻塞(or睡眠,不可中断睡眠和可中断睡眠) 就绪 运行 停止 中止

    linux内核为需要共享资源的多个进程提供的互斥和同步机制:

    1. 什么是互斥?什么是同步?
      互斥指的是保证不同进程对同一资源的访问不冲突的机制
      同步指的是实现不同进程对同一资源的访问顺序的机制

    2. 为什么需要同步机制?
      + 多个并发进程对共享资源的争用
      + 中断,异常处理,内核态抢占导致进程以交错的方式运行,可能对关键数据结构交错修改,
      导致这些数据结构状态的不一致,导致系统的崩溃

      因此,linux系统中广义的进程并发包括:
      a) 中断和异常,源程序和中断处理程序访问同一个临界资源
      b) 内核态抢占,更高优先级的进程在当前进程未结束时被调度,访问同一临界资源
      c) 多个处理器上进程的并发

      疑问: 难道用户态抢占不会造成并发吗?

      内核态抢占: 当前进程进入内核态后,尚未返回到用户态,更高优先级的进程被调度执行
      用户态抢占: 当前进程进入内核态后,cpu发现需要执行另一个更高优先级的进程,但直到当前进程
      返回到用户态后高优先级进程才被调度执行

    3. linux内核提供了哪些同步机制?

      临界区: 对共享资源进行访问的代码片段,需要在进入临界区前启用同步机制,离开后释放

      1) 禁用中断(单核不可抢占系统)
      2) 自旋锁(多处理器系统,需要辅助禁用中断,禁用内核态抢占)

      设计目的: 在多处理器系统中保护共享数据

      实现方法: 使用全局变量V表示锁,V=1时锁定,V=0时解锁.若处理器A上的代码要进入临界区,首先
      读取V的值,若V=1,则忙等(自旋,运行状态,占用cpu时间),否则设置V=1,进入临界区,离开临界区时
      设置V=0,这里对V的测试和设置为原子操作

      实现细节:
      对所有系统,禁用中断,因为中断处理程序和当前程序访问同一全局资源时,导致死锁.当前进程拿到了
      共享资源的锁,此时发生中断,中断处理进程恰好也要访问该共享资源,由于该锁已被占有,因此
      中断处理进程进入忙等状态,无法返回,则当前进程处于没有执行完的状态,也不会释放锁.导致死锁

      a) 对单核系统,锁定只是禁用内核态抢占,解锁只是启用内核态抢占
      b) 对多核系统,锁定时首先禁止内核态抢占,然后尝试锁定,锁定失败时执行死循环等待自旋锁被释放,
      解锁时首先释放自旋锁,然后使能内核态抢占

      为什么需要禁用内核态抢占?
      自旋的含义是持有cpu时间不松手,一直处于运行状态,如果允许其他进程抢占
      内核,则当前进程就不能保持自旋状态

      存在问题:
      a) 忙等占用cpu,当共享资源锁定时间很短时较高效,但共享资源锁定时间较长或不确定时浪费cpu资源
      b) 未对读和写操作进行区分,影响并发性能

      变体:
      a) 读写自旋锁: 允许多个进程同时读,同一时刻只允许一个进程写
      (不互斥: 读-读, 互斥: 读-写,写-读,写-写)
      b) 顺序自旋锁: 放宽 读-写,避免因为长时间的读导致写进程饿死

        思路: 写进程拥有更高的优先级,写锁定请求出现时,立即满足写锁定的请求,无论此时
              是否有读进程. 
                                                           
        实现: 读不加锁,写加锁.                                           
              为保证读取时不会因为写入者的出现导致共享数据的更新,在读取者和写入者
              之间引入整型变量,称为顺序值sequence,读取者读取前后分别读取该sequence,
              如果不一致,说明发生了数据更新,读取操作无效.注意被保护的共享资源不能
              含有指针,因为写进程可能使得指针失效,读进程再访问该指针时会出错
      

      3) 信号量(seamaphore)

      解决问题: 自旋锁上锁失败时自旋运行占用cpu时间

      解决方法:
      a) 进程上锁失败时主动释放cpu资源,进入可中断睡眠状态(不再占用cpu时间),阻塞
      在共享资源的等待队列,其他进程释放共享资源时发出信号唤醒该队列中的一个进程
      b) 共享资源可允许多个进程访问,信号量使用计数器实现,并提供两个原子操作up(),
      down()(原子操作要么全执行,要么全不执行),如果操作后的结果不小于0(操作前不
      小于1)则获得信号量,否则进入等待队列.访问完毕后调用up()释放信号量
      c) 信号量不需要辅助禁用内核态抢占,因为被抢占后该进程进入可中断睡眠状态,效果
      等同于上锁失败,猜测被抢占时仍然保留有锁,因为锁机制是为了保护共享资源,而
      进程抢占是为了满足实时性的要求

      疑问:

      1. 信号量如何解决多个进程同时进入临界区的冲突问题?
        信号量并不能解决,需要额外增加多个同时进入临界区的进程之间的互斥机制
      2. 信号量不用禁用中断吗?如果不用禁用是否是因为中断可以正常获得锁或者进入睡眠而不会
        造成死锁?
      3. 中断处理程序中不能使用信号量,因为信号量可能会导致中断处理程序进入睡眠,而中断处理程序
        不是一个独立的进程(依附于原进程),无法被信号唤醒
     存在问题:未对读和写操作进行区分,影响并发性能
     变体:                                                               
    	1) 读写信号量                                                       
           实现: 读-读不互斥,进程无法获得锁时,睡眠并进入等待队列,唤醒时按先进先出顺序,
                 如果唤醒的第一个是读进程,则继续唤醒下一个读进程,直到遇见一个写进程停止
        2) 互斥量: 计数器的值为1,一次只能允许一个进程进入临界区            
    

    4) RCU(待学习)

2. 线程

线程是进程内的实体,一个进程可以包含一个或多个线程,线程之间独享与栈相关的资源,共享
与栈无关的资源,独享的资源包括:
1) 栈
2) 寄存器(已加载进寄存器的所有程序指令)
3) 程序计数器(执行到指令的哪个位置)
4) 状态字(线程切换时寄存器的状态,用户线程切换回来后恢复现场)

引入线程的目的是提高任务执行的灵活性,因为线程之间共享进程的资源,更加轻量化

可重入and线程安全
可重入: 针对函数执行转入中断处理或异常处理的情况.如果中断处理函数恰好也调用了该函数,
则称为重入,如果中断调用返回后不会对该函数的运行造成影响,则称为可重入函数.

可重入函数必须是无状态函数,不使用任何在函数间共享的数据.
1) 不在函数内部使用静态或全局数据
2) 不返回静态或全局数据
3) 只使用本地数据,或制作全局数据的本地拷贝来保护全局数据
4) 不调用不可重入函数 —!!!—(malloc, free, 标准IO库函数)

线程安全: 针对多线程环境执行同一个函数可能导致的程序运行逻辑错误的情形.
可以使用共享数据,只要保证对共享数据的访问是互斥的即可,常用加锁实现.

可重入函数 is 线程安全函数, 线程安全函数 not necessarily 可重入函数

3. linux中进程和线程的区别

  1. linux使用进程的系统调用来创建线程,其内部并不区分进程和线程,线程相关的函数是库函数,
    面向用户,而非系统调用
  2. 进程可看成只包含一个线程的进程
  3. 进程是最小的资源分配单位,线程是最小的执行单位和调度的基本单位

4. linux系统与用户程序的关系

  1. linux系统的职责是管理用户程序,即调度(Scheduler, Dispatcher),提供任务切换的基本服务.
  2. 用户程序的访问权限需要受到限制,如隔离不同程序的内存地址空间,禁止访问外围设备,这些
    访问统一由操作系统提供,因此将程序占用cpu的状态被分为两级: 用户态和内核态
  3. 内核态: 程序可访问所有的内存数据,包括外围设备,如硬盘,网卡,也可以主动切换到另一个程序
    用户态: 程序只能访问受限的内存,不允许访问外围设备,占用的cpu资源可被其他程序获取
  4. 内核态到用户态的切换的场景
    a) 系统调用(用户主动操作),比如读写文件,socket操作,进程间通信,创建进程
    b) 异常,如缺页异常
    c) 外围设备的中断(硬中断,接作用于cpu,比如硬盘告诉cpu我数据准备好了,你可以读了)

5. linux系统创建进程的方式

  1. 开机时,内核只创建一个init进程
  2. linux内核不提供直接建立新进程的系统调用,剩下的进程都是init进程通过fork机制建立的
    新进程通过老进程复制自身得到,即为fork的原理,fork是系统调用
  3. 进程存活于内存中,每个进程在内存中都有自己专属的内存空间
  4. 进程调用fork时,linux在内存中开辟一块新的内存空间给新的进程,并将老进程空间的内容复制
    到新的空间中,此后2个进程同时运行
  5. 老进程成为新进程的父进程,一个进程除了有PID外,还有一个PPID存储父进程的PID
  6. 所有运行的进程构成了以init为根的树状结构
  7. 子进程终结时,通知父进程,并清空自己占据的内存,并在内核里留下自己的退出信息(exit
    code, 为0表示顺利结束,>0表示有错误或异常)
  8. 父进程得知子进程终结时,有责任对子进程使用wait系统调用,从内核中取出子进程的退出信息,
    并清空该信息在内核中占据的空间
  9. 若父进程早于子进程终结,子进程成为孤儿进程
  10. 孤儿进程会被过继给init进程,init进程负责该子进程终结时调用wait函数
  11. 当父进程未能调用wait函数时,子进程的退出信息滞留在内核中,子进程成为僵死进程,大量
    僵死进程累积时,内存空间会被挤占

你可能感兴趣的:(linux)