1、进程、线程与协程

一文读懂什么是进程、线程、协程
进程:进程是系统进行资源分配和调度的一个独立单位,是系统中的并发执行的单位。进程是程序的⼀次执⾏,该程序可以与其他程序并发执⾏; 进程有运⾏、阻塞、就绪三个基本状态; 进程调度算法:先来先服务调度算法、短作业优先调度算法、⾮抢占式优先级调度算法、抢占式优先级调度算法、 ⾼响应⽐优先调度算法、时间⽚轮转法调度算法;
线程:线程是进程的一个实体,也是 CPU 调 度和分派的基本单位,它是比进程更小的能独立运行的基本单位,有时又被称为轻权进程或轻量级进程。
协程:协程是一种用户态的轻量级线程。协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下⽂和栈保存到其他地⽅,在切换回来的时候, 恢复先前保存的寄存器上下⽂和栈。开销远远⼩于线程;不需要多线程的锁机制;image.png

2、进程与线程的区别与联系

进程、线程和协程之间的区别和联系

  • 调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位;
  • 资源拥有:进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源
  • 系统开销:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行;
  • 并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行;
  • 地址空间:线程是进程内的一个执行单元,进程内至少有一个线程,它们共享进程的地址空间,而进程有自己独立的地址空间
  • 独立性:每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口,但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
  • ⼀个进程崩溃后,在保护模式下不会对其他进程产⽣影响,但是⼀个线程崩溃整个进程崩溃,所以多进程⽐多线程健壮;

联系

  • 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程;
  • 资源分配给进程,同一进程的所有线程共享该进程的所有资源;
  • 处理机分给线程,即真正在处理机上运行的是线程;
  • 线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步

    3、协程和线程的区别

  • 一个线程可以多个协程,一个进程也可以单独拥有多个协程。

  • 线程进程都是同步机制,而协程则是异步
  • 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态

协程特点

  • 极高的执行效率:因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显;
  • 不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

    4、为什么要有线程和协程?

    线程:进一步提高并发性。
    进程可以使多个程序并发执行,以提高资源的利用率和系统的吞吐量,但是其带来了一些缺点:
  1. 进程在同一时间只能干一件事情;
  2. 进程在执行的过程中如果阻塞,整个进程就会被挂起,即使进程中有些工作不依赖与等待的资源,仍然不会执行。

基于以上的缺点,操作系统引入了比进程粒度更小的线程,作为并发执行的基本单位,从而减少程序在并发执行时所付出的时间和空间开销,提高并发性能。
协程:解决并发问题,让 「协作式多任务」 实现起来更加方便。

5、进程基本状态

1、就绪状态。某些进程“万事俱备”(必要资源),只差CPU。(就绪队列)
2、执行状态。某进程占有CPU并在CPU上执行其程序。
3、阻塞状态。某些进程由于某种原因不能继续运行下去,等待处 理问题。也称为等待状态或封锁状态。如:请求I/O。(多个等待队列)
image.png
1)就绪-→执行:
对就绪状态的进程,当进程调度程序按一种选定的策略从中选中一个就绪进程,为之分配了处理机后,该进程便由就绪状态变为执行状态;
2)执行-→阻塞:
正在执行的进程因发生某等待事件而无法执行,则进程由执行状态变为阻塞状态。
如:进程提出输入/输出请求而变成等待外部设备传输信息的状态,进程申请资源(主存空间或外部设备)得不到满足时变成等待资源状态,进程运行中出现了故障(程序出错或主存储器读写错等)变成等待干预状态等等;
3)阻塞-→就绪:
处于阻塞状态的进程,在其等待的事件已经完成,如输入/输出完成,资源得到满足或错误处理完毕时,处于等待状态的进程并不马上转入执行状态,而是先转入就绪状态,然后再由系统进程调度程序在适当的时候将该进程转为执行状态;
4)执行→就绪:
正在执行的进程,因时间片用完而被暂停执行,或在采用抢先式优先级调度算法的系统中,当有更高优先级的进程要运行而被迫让出处理机时,该进程便由执行状态转变为就绪状态。

6、进程通信有哪几种方式

进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。IPC 的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、信号、Streams 等。其中 Socket 和 Streams 支持不同主机上的两个进程 IPC。
信号
信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生
管道

  1. 它是半双工的,具有固定的读端和写端;
  2. 它只能用于父子进程或者兄弟进程之间的进程的通信;
  3. 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的 read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。

命名管道
mkmod或mkfifo创建

  1. FIFO 可以在无关的进程之间交换数据,与无名管道不同;
  2. FIFO 有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。存储在硬盘上。

消息队列

  1. 消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符 ID 来标识;消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  2. 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级
  3. 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除;
  4. 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。

信号量

  1. 信号量(semaphore)是一个计数器。用于实现进程间的互斥与同步,而不是用于存储进程间通信数据;
  2. 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存;
  3. 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作;
  4. 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数;
  5. 支持信号量组。

共享内存
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的IPC方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与信号量,配合使用来实现进程间的同步和通信。

  1. 共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区;
  2. 共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。

Socket
适用于不同机器间进程通信,在本地也可作为两个进程通信的方式。
信号
用于通知接收进程某个事件已经发生,比如按下ctrl + C就是信号。

7、进程终止的几种方式?

  • 正常退出(自愿的)
  • 错误退出(自愿的)
  • 严重错误退出(非自愿的)
  • 被其他进程杀死(非自愿的)

正常退出
多数进程是由于完成了工作而终止。当编译器完成了所给定程序的编译之后,编译器会执行一个系统调用告诉操作系统它完成了工作。这个调用在 UNIX 中是 exit ,在 Windows 中是 ExitProcess。
面向屏幕中的软件也支持自愿终止操作。字处理软件、Internet 浏览器和类似的程序中总有一个供用户点击的图标或菜单项,用来通知进程删除它所打开的任何临时文件,然后终止。
错误退出
进程发生终止的第二个原因是发现严重错误,例如,如果用户执行如下命令cc foo.c
为了能够编译 foo.c 但是该文件不存在,于是编译器就会发出声明并退出。在给出了错误参数时,面向屏幕的交互式进程通常并不会直接退出,因为这从用户的角度来说并不合理,用户需要知道发生了什么并想要进行重试,所以这时候应用程序通常会弹出一个对话框告知用户发生了系统错误,是需要重试还是退出。
严重错误退出
进程终止的第三个原因是由进程引起的错误,通常是由于程序中的错误所导致的。例如,执行了一条非法指令,引用不存在的内存,或者除数是 0 等。在有些系统比如 UNIX 中,进程可以通知操作系统,它希望自行处理某种类型的错误,在这类错误中,进程会收到信号(中断),而不是在这类错误出现时直接终止进程。
被其他进程杀死
某个进程执行系统调用告诉操作系统杀死某个进程。在 UNIX 中,这个系统调用是 kill。在 Win32 中对应的函数是 TerminateProcess(注意不是系统调用)。
具体的:
1、main函数的自然返回,return
2、调用exit函数,属于c的函数库
3、调用_exit函数,属于系统调用
4、调用abort函数,异常程序终止,同时发送SIGABRT信号给调用进程。
5、接受能导致进程终止的信号:ctrl+c (^C)、SIGINT(SIGINT中断进程)
exit和_exit的区别
image.png

8、守护进程、僵尸进程和孤儿进程介绍一下?

守护进程指在后台运行的,没有控制终端与之相连的进程。它独立于控制终端,周期性地执行某种任务。Linux的大多数服务器就是用守护进程的方式实现的,如web服务器进程http等。
怎么创建?
1)创建子进程,父进程退出。fork()
2)在子进程中创建新会话。用setsid()创建一个新的会话,并担任会话组的组长,具体有三个作用让进程摆脱原会话、原进程和原控制终端的控制;
3)禁止进程重新打开控制终端。经过以上步骤,进程已经成为一个无终端的会话组长,但是它可以重新申请打开一个终端。为了避免这种情况发生,可以通过使进程不再是会话组长来实现。再一次通过fork()创建新的子进程,使调用fork的进程退出。
4)关闭不再需要的文件描述符。子进程从父进程继承打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。首先获得最高文件描述符值,然后用一个循环程序,关闭0到最高文件描述符值的所有文件描述符。
5)将当前目录更改为根目录。
6)重设文件权限掩码。子进程从父进程继承的文件创建屏蔽字可能会拒绝某些许可权。为防止这一点,使用unmask(0)将屏蔽字清零。
7)处理SIGCHLD信号。对于服务器进程,在请求到来时往往生成子进程处理请求。如果子进程等待父进程捕获状态,则子进程将成为僵尸进程(zombie),从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将SIGCHLD信号的操作设为SIG_IGN。这样,子进程结束时不会产生僵尸进程。image.png
另外一种说法?
1) 在⽗进程中执⾏fork并exit推出;
2) 在⼦进程中调⽤setsid函数创建新的会话;
3) 在⼦进程中调⽤chdir函数,让根⽬录 ”/” 成为⼦进程的⼯作⽬录;4) 在⼦进程中调⽤umask函数,设置进程的umask为0;
5) 在⼦进程中关闭任何不需要的⽂件描述符。
孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。
设置僵尸进程的目的是维护子进程的信息,以便父进程在以后某个时候获取。这些信息至少包括进程ID,进程的终止状态,以及该进程使用的CPU时间,所以当终止子进程的父进程调用wait或waitpid时就可以得到这些信息。

9、怎么避免僵尸进程?

  • 通过signal(SIGCHLD, SIG_IGN)通知内核对子进程的结束不关心,由内核回收。如果不想让父进程挂起,可以在父进程中加入一条语句:signal(SIGCHLD,SIG_IGN);表示父进程忽略SIGCHLD信号,该信号是子进程退出的时候向父进程发送的。
  • 父进程调用wait/waitpid等函数等待子进程结束,如果尚无子进程退出wait会导致父进程阻塞。waitpid可以通过传递WNOHANG使父进程不阻塞立即返回。
  • 如果父进程很忙可以用signal注册信号处理函数,在信号处理函数调用wait/waitpid等待子进程退出。
  • 通过两次调用fork。父进程首先调用fork创建一个子进程然后waitpid等待子进程退出,子进程再fork一个孙进程后退出。这样子进程退出后会被父进程等待回收,而对于孙子进程其父进程已经退出所以孙进程成为一个孤儿进程,孤儿进程由init进程接管,孙进程结束后,init会等待回收。

第一种方法忽略SIGCHLD信号,这常用于并发服务器的性能的一个技巧因为并发服务器常常fork很多子进程,子进程终结之后需要服务器进程去wait清理资源。如果将此信号的处理方式设为忽略,可让内核把僵尸子进程转交给init进程去处理,省去了大量僵尸进程占用系统资源。

10、介绍一下几种典型的锁?

互斥锁
一次只能一个线程拥有互斥锁,其他线程只有等待。
互斥锁在抢锁失败的情况下主动放弃CPU进入睡眠状态直到锁的状态改变时再唤醒,而操作系统负责线程调度。
对于互斥锁加锁失败而阻塞的现象,是由操作系统内核实现的。当加锁失败时,内核会将线程置为睡眠状态,等到锁被释放后,内核会在合适的时机唤醒线程,当这个线程成功获取到锁后,于是就可以继续执行了。
所以,互斥锁加锁失败时,会从用户态陷入到内核态,让内核帮我们完成切换线程,虽然简化了使用锁的难度,但是存在一定的性能开销成本,即有两次线程上下文切换:
当线程加锁失败时,内核会把线程的状态从运行状态设置为睡眠状态,然后把CPU切换给其他进程运行;
当锁被释放时,之前睡眠状态的进程会变为就绪状态,然后内核会在合适的时候,把CPU切换给该线程运行。
如果你锁住的代码执行时间比较短,那可能上下文切换的时间比你锁住的代码执行时间还要长。
所以,如果你能确定被锁住的代码执行时间很短,就不应该使用互斥锁,而应该使用自旋锁,否则使用互斥锁。
自旋锁
如果进线程无法取得锁,进线程不会立刻放弃CPU时间片,而是一直循环尝试获取锁,直到获取为止。如果别的线程长时期占有锁,那么自旋就是在浪费CPU做无用功,但是自旋锁一般应用于加锁时间很短的场景,这个时候效率比较高。
旋锁与互斥锁使用层面比较类似,但实现层面完全不同:当加锁失败时,互斥锁用线程切换来应对,自旋锁则用忙等待来应对。
它们俩是锁的最基本处理方式,更高级的锁都会选择其中一个来实现,比如读写锁既可以选择基于互斥锁实现,也可以选择基于自旋锁实现。
互斥锁Mutex可以分为递归锁(recursive mutex)和⾮递归锁(non-recursive mutex)。可递归锁也可称为可重⼊锁 (reentrant mutex),⾮递归锁⼜叫不可重⼊锁(non-reentrant mutex)。⼆者唯⼀的区别是,同⼀个线程可以多次获取同⼀个递归锁,不会产⽣死锁。⽽如果⼀个线程多次获取同⼀个⾮递归锁,则会产⽣死锁。
读写锁

  • 多个读者可以同时进行读
  • 写者必须互斥(只允许一个写者写,也不能读者写者同时进行)

读写锁由读锁和写锁两部分构成,如果只读取共享资源用读锁加锁,如果要修改共享资源则用写锁加锁。所以,读写锁适用于能明确区分读操作和写操作的场景。
读写锁工作原理:当写锁没有被线程持有时,多个线程能够并发地持有读锁,这大大提高了共享资源的访问效率,因为读锁是用于读取共享资源的场景,所以多个线程同时持有读锁也不会破坏共享资源。但是,一旦写锁被线程持有后,读线程的获取锁的操作会被阻塞,而且其他写线程的获取写锁的操作也会被阻塞。
所以说,写锁是独占锁,因为任何时刻只能有一个线程持有写锁,类似互斥锁和自旋锁,而读锁是共享锁,因为读锁可以被多个线程同时持有。所以,读写锁在读多写少的场景,能发挥出优势。
另外,根据实现的不同,读写锁可以分为读优先锁和写优先锁。
读优先锁对于读线程并发性更好,但是如果一直有读线程获取读锁,那么写线程将永远获取不到写锁,这就造成了写线程饥饿的现象。
写优先锁可以保证写线程不会饿死,但是如果一直有写线程获取写锁,读线程也会被饿死。
还有一种公平读写锁:用队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁即可,这样读线程仍然可以并发,也不会出现饥饿的现象。
条件变量
互斥锁一个明显的缺点是他只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,他常和互斥锁一起使用,以免出现竞态条件。当条件不满足时,线程往往解开相应的互斥锁并阻塞线程然后等待条件发生变化。一旦其他的某个线程改变了条件变量,他将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。总的来说互斥锁是线程间互斥的机制,条件变量则是同步机制。
悲观锁和乐观锁
前面提到的互斥锁、自旋锁、读写锁,都属于悲观锁。
悲观锁认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁。
悲观锁(多锁,适合写多,竞争激烈的场景)在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
悲观锁主要分为共享锁或排他锁
(1)共享锁【Shared lock】又称为读锁,简称S锁。顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
(2)排他锁【Exclusive lock】又称为写锁,简称X锁。顾名思义,排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据行读取和修改。
而乐观锁认为冲突的概率很低,它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。
乐观锁全程没有加锁,所以它也叫无锁编程。
乐观锁虽然去除了加锁解锁的操作,但是一旦发生冲突,重试的成本非常高,所以只有在冲突概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁。
乐观锁(少锁,适用于写少,竞争不激烈的应用场景,可以提高吞吐量)

11、进程创建过程?fork怎么用?

进程创建
1. 为新进程分配一个唯一的进程标识号,并申请一个空白PCB。若PCB申请失败,则创建失败。
2. 为进程分配资源,为新进程的程序和数据及用户栈分配必要的内存空间。若资源不足,则并不是创建失败,而是处于阻塞态,等待内存资源。
3. 初始化PCB,主要包括初始化标志信息、初始化处理机状态信息和初始化处理控制信息,以及设置进程的优先级等。
4. 若进程就绪队列能够接纳新进程,则将新进程插入就绪队列,等待被调度运行。
linux函数
父进程调用fork()、vfork()、clone()中的任一方法创建新进程,这些方法对应的系统调用入口分别为sys_fork()、sys_vfork()、sys_clone()。这些系统调用内部都会调用do_fork()函数完成进程创建,只是携带的参数不同。
fork()
pid_t fork(void); //void代表没有任何形式参数 。
除了0号进程(系统创建的)之外,linux系统中都是由其他进程创建的。创建新进程的进程,即调⽤fork函数的进程为⽗进程,新建的进程为⼦进程。
fork函数不需要任何参数,对于返回值有三种情况:
① 对于⽗进程,fork函数返回新建⼦进程的pid;
② 对于⼦进程,fork函数返回 0;
③ 如果出错, fork 函数返回 -1.
vfork创建的⼦进程与⽗进程共享数据段,⽽且由vfork创建的⼦进程将先于⽗进程运⾏.
linux上创建线程⼀般使⽤的是pthread库,linux也给我们提供了创建线程的系统调⽤,就是clone;

12、一个进程可以创建多少个线程?

一个进程可以创建多少线程
理论上,一个进程可用虚拟空间是2G,默认情况下,线程的栈的大小是1MB,所以理论上最多只能创建
2048个线程。如果要创建多于2048的话,必须修改编译器的设置。
因此,一个进程可以创建的线程数由可用虚拟空间和线程的栈的大小共同决定,只要虚拟空间足够,那
么新线程的建立就会成功。如果需要创建超过2K以上的线程,减小你线程栈的大小就可以实现了,虽然
在一般情况下,你不需要那么多的线程。过多的线程将会导致大量的时间浪费在线程切换上,给程序运
行效率带来负面影响。

  • 32 位系统,用户态的虚拟空间只有 3G,如果创建线程时分配的栈空间是 10M,那么一个进程最多只能创建 300 个左右的线程。
  • 64 位系统,用户态的虚拟空间大到有 128T,理论上不受虚拟内存大小的限制,而受系统的参数或性能限制。

    13、父子进程之间怎么通信?

    在 Linux 系统中实现⽗⼦进程的通信可以采⽤ pipe() 和 fork() 函数进⾏实现;
    管道:是指⽤于连接⼀个读进程和⼀个写进程,以实现它们之间通信的共享⽂件,⼜称 pipe ⽂件。
    写进程在管道的尾端写⼊数据,读进程在管道的⾸端读出数据。

    14、进程、程序和作业的区别和联系是什么?

    进程和作业
    一个进程是一个程序对某个数据集的执行过程,是分配资源的基本单位。作业是用户需要计算机完成的某项任务,是要求计算机所做工作的集合。一个作业的完成要经过作业提交、作业收容、作业执行和作业完成4个阶段。而进程是对已提交完毕的程序所执行过程的描述,是资源分配的基本单位。其主要区别如下。
    (1)作业是用户向计算机提交任务的任务实体。在用户向计算机提交作业后,系统将它放入外存中的作业等待队列中等待执行。而进程则是完成用户任务的执行实体,是向系统申请分配资源的基本单位。任一进程,只要它被创建,总有相应的部分存在于内存中。
    (2)一个作业可由多个进程组成,且必须至少由一个进程组成,反过来则不成立。
    (3)作业的概念主要用在批处理系统中,像UNIX这样的分时系统中就没有作业的概念。而进程的概念则用在几乎所有的多道程序系统中。
    作业与进程最主要的区别是:前者是由用户提交,后者是由系统自动生成;前者以用户任务为单位,后者是操作系统控制的单位。
    作业、进程和程序之间的联系
    一个作业通常包括程序、数据和操作说明书3部分。每一个进程由PCB、程序和数据集合组成。这说明程序是进程的一部分,是进程的实体。因此,一个作业可划分为若干个进程来完成,而每一个进程由其实体,程序和数据集合。

  • 进程是程序的一次运行活动,属于一种动态的概念。 程序是一组有序的静态指令,是一种静 态 的 概 念。

  • 一 个 进 程 可 以 执 行 一 个 或 多个 程 序。
  • 程 序 可 以 作 为 一 种 软 件 资 源长 期 保 持 着, 而 进 程 则 是 一 次 执 行 过 程, 它 是 暂时 的, 是 动 态 地 产 生 和 终 止 的。
  • 进程还具有并发性和交往性,这也与程序的封闭性不同。
  • 进程由程序和数据两部分组成,进程是竞争计算机系统有限资源的基本单位.
  • 进程具有创建其他进程的功能;而程序没有。

    15、父进程、子进程、进程组、作业和会话都是什么?

    父进程
    已创建一个或多个子进程的进程。
    子进程
    由fork创建的新进程被称为子进程(child process)。这2个进程共享代码空间,但是数据空间是互相独立的,子进程数据空间中的内容是父进程的完整拷贝,指令指针也完全相同,子进程拥有父进程当前运行到的位置(两进程的程序计数器pc值相同,也就是说,子进程是从fork返回处开始执行的),但有一点不同,如果fork成功,子进程中fork的返回值是0,父进程中fork的返回值是子进程的进程号,如果fork不成功,父进程会返回错误。
    子进程从父进程继承的有:
    1.进程的资格(真实(real)/有效(effective)/已保存(saved)用户号(UIDs)和组号(GIDs))
    2.环境(environment)
    3.堆栈
    4.内存
    5.进程组号
    独有:
    1.进程号;
    2.不同的父进程号(即子进程的父进程号与父进程的父进程号不同,父进程号可由getppid()得到);
    3.资源使用(resource utilizations)设定为0
    进程组
    进程组就是多个进程的集合,其中肯定有一个组长,其进程PID等于进程组的PGID。只要在某个进程组
    中一个进程存在,该进程组就存在,这与其组长进程是否终止无关。
    作业
    shell分前后台来控制的不是进程而是作业(job)或者进程组(Process Group)。
    一个前台作业可以由多个进程组成,一个后台也可以由多个进程组成,shell可以运行一个前台作业和任
    意多个后台作业,这称为作业控制
    为什么只能运行一个前台作业?
    答:当我们在前台新起了一个作业,shell就被提到了后台,因此shell就没有办法再继续接受我们的指令
    并且解析运行了。 但是如果前台进程退出了,shell就会有被提到前台来,就可以继续接受我们的命令并
    且解析运行。
    作业与进程组的区别:如果作业中的某个进程有创建了子进程,则该子进程是不属于该作业的。
    一旦作业运行结束,shell就把自己提到前台(子进程还存在,但是子进程不属于作业),如果原来的前
    台进程还存在(这个子进程还没有终止),他将自动变为后台进程组。
    会话
    会话(Session)是一个或多个进程组的集合。一个会话可以有一个控制终端。在xshell或者WinSCP中打
    开一个窗口就是新建一个会话。

    16、线程共享的资源是什么?

    ⼀个进程中的所有线程共享该进程的地址空间,但它们有各⾃独⽴、私有的栈(stack),Windows 线程的缺省堆栈⼤⼩为1M。
    image.png
    线程私有:线程栈,寄存器,程序寄存器
    共享:堆,地址空间,全局变量,静态变量
    进程私有:地址空间,堆,全局变量,栈,寄存器
    共享:代码段,公共数据,进程⽬录,进程ID

    17、线程比进程有哪些优势?

    1) 线程在程序中是独⽴的,并发的执⾏流,但是,进程中的线程之间的隔离程度要⼩;
    2) 线程⽐进程更具有更⾼的性能,这是由于同⼀个进程中的多个线程将共享同⼀个进程虚拟空间;
    3) 当操作系统创建⼀个进程时,必须为进程分配独⽴的内存空间,并分配⼤量相关资源;

    18、什么时候⽤多进程?什么时候⽤多线程?

  • 频繁修改:需要频繁创建和销毁的优先使用多线程

  • 计算量:需要大量计算的优先使用多线程因为需要消耗大量CPU资源且切换频繁
  • 相关性:任务间相关性比较强的用多线程,相关性比较弱的用多进程。因为线程之间的数据共享和同步比较简单。
  • 多分布:可能要扩展到多机分布的用多进程,多核分布的用多线程

    19、进程同步/线程同步介绍下?

    进程的同步、互斥、通信的区别,进程与线程同步的区别

    主要任务

    协调合作进程的执行次序,使并发执行的各进程间能按照一定规则(或时序)有效的共享资源,以及相互协作,从而使程序的执行具有可再现性 。

    制约关系
  • 间接相互制约关系:源于资源共享,由于共享同一资源而形成的制约关系,又称互斥。

  • 直接相互制约关系:源于进程合作,进程A的执行结果导致进程B的执行,进程B的执行影响 ,又称同步。

    临界资源

    各进程必须使用互斥方式共享的资源

    遵循原则

    a 空闲让进:临界资源空闲,新到进程请求应立即响应
    a 忙则等待:临界资源正在被访问,新的资源申请进程必须等待
    a 有限等待:有限长度时间内进入临界区,以避免“死等”
    a 让权等待:执行态的进程需要等待临界资源时,应立即放弃CPU,以免“忙等”

    实现方法

    临界区、互斥对象、信号量、事件对象;其中临界区和互斥对象主要⽤于互斥控制,信号量和事件对象主要⽤于同步控制;
    临界区
    进程中访问临界软硬件资源的代码称为临界区。通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。
    为了保证临界资源的正确使用,可以把临界资源的访问过程分成四个部分:

  • 进入区。为了进入临界区使用临界资源,在进入区要检查可否进入临界区,如果可以进入临界区,则应设置正在访问临界区的标志,以阻止其他进程同时进入临界区。

  • 临界区。进程中访问临界资源的那段代码,又称临界段。
  • 退出区。将正在访问临界区的标志清除。
  • 剩余区。代码中的其余部分

优点:保证在某一时刻只有一个线程能访问数据的简便办法
缺点:虽然临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。
互斥量(Mutex):为协调共同对一个共享资源的单独访问而设计的。
互斥量跟临界区很相似,比临界区复杂,互斥对象只有一个,只有拥有互斥对象的线程才具有访问资源的权限。
优点:使用互斥不仅仅能够在同一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享。
缺点:①互斥量是可以命名的,也就是说它可以跨越进程使用,所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量。因为互斥量是跨进程的互斥量一旦被创建,就可以通过名字打开它。
②通过互斥量可以指定资源被独占的方式使用,但如果有下面一种情况通过互斥量就无法处理,比如现在一位用户购买了一份三个并发访问许可的数据库系统,可以根据用户购买的访问许可数量来决定有多少个线程/进程能同时进行数据库操作,这时候如果利用互斥量就没有办法完成这个要求,信号量对象可以说是一种资源计数器。
信号量机制
为控制一个具有有限数量用户资源而设计。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。互斥量是信号量的一种特殊情况,当信号量的最大资源数=1就是互斥量了。
优点:适用于对Socket(套接字)程序中线程的同步。(例如,网络上的HTTP服务器要对同一时间内访问同一页面的用户数加以限制,只有不大于设定的最大用户数目的线程能够进行访问,而其他的访问企图则被挂起,只有在有用户退出对此页面的访问后才有可能进入。)
缺点:①信号量机制必须有公共内存,不能用于分布式操作系统,这是它最大的弱点;
②信号量机制功能强大,但使用时对信号量的操作分散, 而且难以控制,读写和维护都很困难,加重了程序员的编码负担;
③核心操作P-V分散在各用户程序的代码中,不易控制和管理,一旦错误,后果严重,且不易发现和纠正。
事件(Event)
用来通知线程有一些事件已发生,从而启动后继任务的开始。
优点:事件对象通过通知操作的方式来保持线程的同步,并且可以实现不同进程中的线程同步操作。
缺点:

互斥怎么实现?

互斥实现
软件实现

  • 单标志法
  • 双标志先检查法
  • 双标志后检查法
  • Peterson算法

硬件实现

  • 关中断
  • TestAndSet指令(TS指令)
  • swap指令

POSIX信号量:可用于进程同步,也可用于线程同步。
POSIX互斥锁 + 条件变量:只能用于线程同步。

临界区和互斥量的区别

1、临界区只能用于对象在同一进程里线程间的互斥访问;互斥体可以用于对象进程间或线程间的互斥访问。
2、临界区是非内核对象,只在用户态进行锁操作,速度快;互斥体是内核对象,在核心态进行锁操作,速度慢。
3、临界区和互斥体在Windows平台都下可用;Linux下只有互斥体可用
临界区:
首先明白临界区是指一段代码,这段代码是用来访问临界资源的。临界资源可以是硬件资源,也可以是软件资源。但它们有一个特点就是,一次仅允许一个进程或线程访问。当有多个线程试图同时访问,但已经有一个线程在访问该临界区了,那么其他线程将被挂起。临界区被释放后,其他线程可继续抢占该临界区。
临界区是一种轻量级的同步机制,与互斥和事件这些内核同步对象相比,临界区是用户态下的对象,即只能在同一进程中实现线程互斥。因无需在用户态和核心态之间切换,所以工作效率比较互斥来说要高很多。
互斥量:
互斥量可以看作是信号量的简化版。是一个内核对象,具有线程拥有权,可以实现进程的同步,线程的互斥访问。

20、线程安全是什么?怎么实现?

概念
如果你的代码所在的进程中有多个线程在同时运⾏,⽽这些线程可能会同时运⾏这段代码。如果每次运⾏结果和单线程运⾏的结果是⼀样的,⽽且其他的变量的值也和预期的是⼀样的,就是线程安全的。
引起原因
都是由全局变量及静态变量引起的。
若每个线程中对全局变量、静态变量只有读操作,⽽⽆写操作,⼀般来说,这个全局变量是线程安全的;若有多个线程同时执⾏写操作,⼀般都需要考虑线程同步,否则的话就可能影响线程安全。
实现
① 加锁 利⽤Synchronized或者ReenTrantLock来对不安全对象进⾏加锁,来实现线程执⾏的串⾏化,从⽽保证多线程同时操作对象的安全性,⼀个是语法层⾯的互斥锁,⼀个是API层⾯的互斥锁.
② ⾮阻塞同步来实现线程安全。原理就是:通俗点讲,就是先进性操作,如果没有其他线程争⽤共享数据,那操作就成功了;如果共享数据有争⽤,产⽣冲突,那就再采取其他措施(最常⻅的措施就是不断地量试,知道成功为⽌)。这种⽅法需要硬件的⽀持,因为我们需要操作和冲突检测这两个步骤具备原⼦性。通常这种指令包括CASSC,FAI TAS等。
③ 线程本地化,⼀种⽆同步的⽅案,就是利⽤Threadlocal来为每⼀个线程创造⼀个共享变量的副本来(副本之间是⽆关的)避免⼏个线程同时操作⼀个对象时发⽣线程安全问题。

21、怎么回收线程?有哪几种方法?

  • 等待线程结束:int pthread_join(pthread_t tid, void** retval);

主线程调用,等待子线程退出并回收其资源,类似于进程中wait/waitpid回收僵尸进程,调用pthread_join的线程会被阻塞。

  • tid:创建线程时通过指针得到tid值。
  • retval:指向返回值的指针。
    • 结束线程:pthread_exit(void *retval);

子线程执行,用来结束当前线程并通过retval传递返回值,该返回值可通过pthread_join获得。

  • retval:同上。
    • 分离线程:int pthread_detach(pthread_t tid);

主线程、子线程均可调用。主线程中pthread_detach(tid),子线程中pthread_detach(pthread_self()),调用后和主线程分离,子线程结束时自己立即回收资源。

  • tid:同上。

    22、终端退出,终端运行的进程会怎样

    终端在退出时会发送SIGHUP给对应的bash进程,bash进程收到这个信号后首先将它发给session下面的进程,如果程序没有对SIGHUP信号做特殊处理,那么进程就会随着终端关闭而退出。

    23、如何让进程后台运行

    (1)命令后面加上&即可,实际上,这样是将命令放入到一个作业队列中了
    (2)ctrl + z 挂起进程,使用jobs查看序号,在使用bg %序号后台运行进程
    (3)nohup + &,将标准输出和标准错误缺省会被重定向到 nohup.out 文件中,忽略所有挂断(SIGHUP)信号
    (4)运行指令前面 + setsid,使其父进程编程init进程,不受HUP信号的影响
    (5)将 命令+ &放在()括号中,也可以是进程不受HUP信号的影响

    24、共享是什么?

    共享是指系统中的资源可以被多个并发进程共同使用。
    有两种共享方式:互斥共享和同时共享。
    互斥共享的资源称为临界资源,例如打印机等,在同一时刻只允许一个进程访问,需要用同步机制来实现互斥访问。