进程与线程

一、进程、线程

1.1 进程

进程其实就是一个可执行程序的实例,这句话如何理解呢?可执行程序就是一个可执行文件,文件是一个静态的概念,存放磁盘中,如果可执行文件没有被运行,那它将不会产生什么作用,当它被运行之后,它将会对系统环境产生一定的影响,所以可执行程序的实例就是可执行文件被运行。进程是一个动态过程,而非静态文件,它是程序的一次运行过程,当应用程序被加载到内存中运行之后它就称为了一个进程,当程序运行结束后也就意味着进程终止,这就是进程的一个生命周期。

1.2 线程

线程是参与系统调度的最小单位。 它被包含在进程之中, 是进程中的实际运行单位。一个线程指的是进程中一个单一顺序的控制流(或者说是执行路线、执行流), 一个进程中可以创建多个线程, 多个线程实现并发运行, 每个线程执行不同的任务。 譬如某应用程序设计了两个需要并发运行的任务 task1 和 task2,可将两个不同的任务分别放置在两个线程中。

1.3 并发、并行、串行

串行和并行没什么好说的,这里主要说一下并发,并发强调的是时分复用,怎么理解呢,就是在执行一个任务的时候可以切出去执行别的任务然后在回来继续执行刚刚中断的任务。交替的做不同的事情。

二、僵尸进程、孤儿进程、守护进程

当一个进程创建子进程之后,它们俩就成为父子进程关系,父进程与子进程的生命周期往往是不相同的,这里就会出现两个问题:
父进程先于子进程结束、子进程先于父进程结束

  • 孤儿进程
    父进程先于子进程结束,也就是意味着,此时子进程变成了一个“孤儿”,我们把这种进程就称为孤儿进程。 在 Linux 系统当中,所有的孤儿进程都自动成为 init 进程(进程号为 1)的子进程, 换言之,某一子进程的父进程结束后,该子进程调用 getppid()将返回 1, init 进程变成了孤儿进程的“养父”。

  • 僵尸进程

    进程结束之后,通常需要其父进程为其“收尸”,回收子进程占用的一些内存资源,父进程通过调用wait()函数回收子进程资源,归还给系统。如果子进程先于父进程结束,此时父进程还未来得及给子进程“收尸”,那么此时子进程就变成了一个僵尸进程。 子进程结束后其父进程并没有来得及立马给它“收尸”, 子进程处于“曝尸荒野”的状态,在这么一个状态下,我们就将子进程称为僵尸进程。
    当父进程调用 wait()为子进程“收尸”后,僵尸进程就会被内核彻底删除。另外一种情况,如果父进程并没有调用 wait()函数然后就退出了,那么此时 init 进程将会接管它的子进程并自动调用 wait(), 故而从系统中移除僵尸进程。如果父进程创建了某一子进程,子进程已经结束,而父进程还在正常运行,但父进程并未调用 wait()回收子进程,此时子进程变成一个僵尸进程。 首先来说,这样的程序设计是有问题的,如果系统中存在大量的僵尸进程,它们势必会填满内核进程表,从而阻碍新进程的创建。需要注意的是,僵尸进程是无法通过信号将其杀死的,即使是“一击必杀”信号 SIGKILL 也无法将其杀死,那么这种情况下,只能杀死僵尸进程的父进程(或等待其父进程终止),这样 init 进程将会接管这些僵尸进程,从而将它们从系统中清理掉!所以,在我们的一个程序设计中,一定要监视子进程的状态变化,如果子进程终止了,要调用 wait()将其回收,避免僵尸进程。

  • 守护进程
    守护进程是一种运行在后台的特殊进程,独立于终端控制并且周期性的执行某种任务或者等待处理某些任务的事件发生,主要有两个特点:(1)长期运行。在系统启动后就开始运行直至关机。普通的进程是在用户登录或运行程序时创建,在运行结束或者用户注销时终止,但守护进程不会受影响。(2)不受终端影响。普通进程的运行都是和运行进程的终端所绑定的,当终端关闭的时候,相应的进程也会结束,但对于守护进程来说,是不受终端影响的,不会被终端所产生的信息所打断。
    那么守护进程有什么用处呢?
    Linux服务器在启动时需要启动很多系统服务,它们向本地和网络用户提供了Linux的系统功能接口,直接面向应用程序和用户。提供这些服务的程序是由运行在后台的守护进程(daemons)来执行的。常见的守护进程包括:系统日志进程syslogd、web服务器httpd、邮件服务器sendmail、数据库服务器mysqld等。

三、进程间通信

在这里插入图片描述
早期的 UNIX IPC 主要有 管道、FIFO、信号
system v IPC 和 POSIX IPC 主要有 信号量、消息队列、共享内存
socket IPC 是 基于 socket(套接字) 进程间通信

3.1 管道

管道分为无名管道(pipe)以及命名管道(FIFO),除了建立、打开、删除的方式不同外,这两种管道几乎是一样的。他们都是通过内核缓冲区实现数据传输。无名管道父子进程通信,命名管道可以实现非父子进程通信

管道的实质是一个内核缓冲区进程以先进先出的方式从缓冲区存取数据:管道一端的进程顺序地将进程数据写入缓冲区,另一端的进程则顺序地读取数据,该缓冲区可以看做一个循环队列,读和写的位置都是自动增加的,一个数据只能被读一次,读出以后再缓冲区都不复存在了。当缓冲区读空或者写满时,有一定的规则控制相应的读进程或写进程是否进入等待队列,当空的缓冲区有新数据写入或慢的缓冲区有数据读出时,就唤醒等待队列中的进程继续读写。操作管道:半双工,读端和写端必须都存在,如果写端关闭,读完之后会返回0。如果读端关闭,写操作会触发异常SIGPIPE。

pipe 与 FIFO 的一个主要区别就是FIFO是有名字的,命名管道的名字相当于一个磁盘索引节点,有了这个文件名,任何进程有相应的权限都可以对其进行访问。

3.2 消息队列

消息队列,一种存在于内存上的数据结构,由内核维护的。也就是存在在内存上用来存放进进程之间通信信息的队列,这个数据结构是可以脱离进程而存在的,也就是说,发送方进程在往消息队列里存放数据块后就可以直接走人了,而接受方也不像管道一样必须等待着发送方发数据,想拿的时候拿就可以了。

消息队列与管道通信相比:

  • 管道是跟随进程的,消息队列是跟随内核的,也就是说进程结束之后,管道就死了,但是消息队列还会存在
  • 管道是文件,文件节点是存放在存放在磁盘上的,因此访问速度慢,消息队列是数据结构,存放在内存,访问速度快
  • 管道的开销机制大。每个管道都要浪费一个文件描述符

3.3 共享内存

首先共享内存是拿出一块内存区域供多个进程同时访问,这样一段存储空间可以被两个或两个以上的进程映射到自己的地址空间中。采用共享内存的进程间通信方式的最大的好处就是效率高,因为进程可以直接读写内存而不涉及数据的拷贝。

但是,使用共享内存的时候切记需要同步,否则会造成读写冲突或是脏读。

3.4 信号量

3.5 socket套接字

与前面几个不同的是,他可以用于不同机器间的进程间的通信

四、线程同步

4.1 必要性

线程同步的目的是为了对共享资源的访问进行保护,保护的目的是为了解决数据一致性问题,进程中的多个线程之间是并发的,并发所带来的问题就是会出现竞争现象。当一个线程修改变量时,其他线程读取这个变量时就会看到不一致的值,为了避免这个问题的发生,提出了线程同步的思想。

4.2 linux下:

4.2.1 互斥锁

互斥锁又叫互斥量,其原理很简单,就是在访问公共资源的时候加一把锁,这样别人就没有访问的权限了,在使用完公共资源的时候在解锁,上锁后,如果有别的线程想对已经加锁的资源再次尝试加锁就会被阻塞,直至当前线程将锁释放掉才会被唤醒。
在使用互斥锁的时候,首先要对互斥锁进行初始化,接着调用一些 API 函数进行加锁解锁操作。这里有一个不阻塞的加锁函数,pthread_mutex_trylock()函数,也就是说当尝试对互斥锁加锁时不会因为已经被加锁而使线程进入阻塞状态。

这里重点说一下互斥锁死锁的现象
(1)当一个线程试图对同一个互斥锁加锁两次时可能会出现死锁,一直被阻塞。
(2)当线程 A 拥有共享资源 a 的互斥锁,线程 B 拥有共享资源 b 的互斥锁,那么当线程 A 想去锁资源 b,而线程 B 想去锁资源 a 时,此时就会发生死锁,会一直被阻塞。

4.2.2 条件变量

条件变量用于自动阻塞线程,直到某个特定的事件发生或某个条件满足为止。 下面说一种场景,在生产者-消费者模式下,生产者这边负责生产产品、而消费者负责消费产品,对于消费者来说,没有产品的时候只能等待产品出来,有产品就使用它。我们使用一个全局变量来维护产品的数量,消费者需要不断的去查询产品数目,此时就会造成CPU 在与资源的浪费,我们可以使用条件变量,条件变量允许一个线程的阻塞直至获取到满足的条件事件发生。例如上述的消费者-生产者模式中,当生产者所生产的产品数量不足以消费者使用的时候允许消费者线程进入阻塞模式。

通常情况下,条件变量是和互斥锁一起搭配使用的。条件的检测是在互斥锁的保护下进行的,也就是说条件本身是受保护的,线程在改变条件状态之前需要锁住互斥锁,否则有可能引发线程不安全的问题。

4.2.3自旋锁

自旋锁与互斥锁本质上都是加锁,但也有一些区别,在于:

  • 实现方式上的区别:互斥锁是基于自旋锁而实现的,所以自旋锁相较于互斥锁更加底层;
  • 开销上的区别:获取不到互斥锁会陷入阻塞状态(休眠) ,直到获取到锁时被唤醒;而获取不到自旋锁会在原地“自旋”,直到获取到锁; 休眠与唤醒开销是很大的, 所以互斥锁的开销要远高于自旋锁、 自旋锁的效率远高于互斥锁; 但如果长时间的“自旋”等待,会使得 CPU 使用效率降低,故自旋锁不适用于等待时间比较长的情况。
  • 使用场景的区别: 自旋锁在用户态应用程序中使用的比较少, 通常在内核代码中使用比较多;因为自旋锁可以在中断服务函数中使用,而互斥锁则不行,在执行中断服务函数时要求不能休眠、不能被抢占(内核中使用自旋锁会自动禁止抢占) , 一旦休眠意味着执行中断服务函数时主动交出了CPU 使用权,休眠结束时无法返回到中断服务函数中,这样就会导致死锁!

自旋锁的不足之处在于:自旋锁一直占用的 CPU,它在未获得锁的情况下,一直处于运行状态(自旋),所以占着 CPU,如果不能在很短的时间内获取锁,这无疑会使 CPU 效率降低。

试图对同一自旋锁加锁两次必然会导致死锁,而试图对同一互斥锁加锁两次不一定会导致死锁,原因在于互斥锁有不同的类型,当设置为 PTHREAD_MUTEX_ERRORCHECK 类型时,会进行错误检查, 第二次加锁会返回错误, 所以不会进入死锁状态。

自旋锁通常用于以下情况: 需要保护的代码段执行时间很短,这样就会使得持有锁的线程会很快释放锁,而“自旋”等待的线程也只需等待很短的时间;在这种情况下就比较适合使用自旋锁,效率高!

4.2.4 读写锁

读写锁不同于上面的两种锁之处在于读写锁有三种状态,读加锁、写加锁、无锁
读写锁有两个规则
当读写锁处于写加锁状态的时候,在锁被解锁之前,所有的加锁线程都会被阻塞
当读写锁处于读加锁状态的时候,以读模式进行加锁的线程都能加锁成功。以写模式加锁的线程会被阻塞

读写锁适用于对共享数据读次数远大于写次数的场景。又叫共享互斥锁。

4.3 windows 下

4.3.1 临界区

临界区(Critical Section)是一段独占对某些共享资源访问的代码,在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。

4.3.2 互斥量

互斥对象和临界区很像,采⽤互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有⼀个,所以能保证公共资源不会同时被多个线程同时访问。当前拥有互斥对象的线程处理完任务后必须将线程交出,以便其他线程访问该资源。

4.3.3 事件

通过通知操作的⽅式来保持线程的同步,还可以⽅便实现对多个线程的优先级⽐较的操作。 事件(Event) 是WIN32提供的最灵活的线程间同步方式,事件可以处于激发状态(signaled or true)或未激发状态(unsignal or false)。

4.3.4 信号量

它允许多个线程在同⼀时刻访问同⼀资源,但是需要限制在同⼀时刻访问此资源的最⼤线程数⽬: 信号量是维护0到指定最大值之间的同步对象。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值