操作系统之进程(面试总结)

2、进程

2.1、进程、线程、协程的区别
进程 线程 协程
定义 资源分配和拥有的基本单位 资源调度的基本单位 用户态的轻量级线程,线程内部调度的基本单位
切换情况 进程CPU环境(栈、寄存器、页表、句柄等)的保存以及新调度的进程CPU环境的设置 保存和设置程序计数器、少量寄存器和栈的内容 先将寄存器上下文和栈保存,后期恢复
切换过程 用户->内核->用户 用户->内核->用户 用户态(没有陷入内核)
拥有资源 CPU资源、内存资源、文件资源等 程序计数器、寄存器、栈和状态字 独有的寄存器上下文和栈
并发性 不同进程之间切换实现并发,各自占有CPU并行 一个进程内部的多个线程并发执行 同一时间只能执行一个协程,而其他协程处于休眠状态,适合多任务进行分时处理
系统开销 切换虚拟地址空间,切换内核栈和硬件上下文,CPU高速缓存失效、页表切换,开销大 切换时只需保存和设置少量寄存器内容,开销较小 直接操作栈则基本没有内核切换开销,可以不加锁的访问全局变量,上下文切换快
通信方面 进程间通信需要借助操作系统 线程间可以直接读写进程数据段(如全局变量)来进行通信 共享内存、消息队列
2.2、并发和并行
  • 并发:同一时间间隔内做多件事情,比如“时间片轮转进程调度算法”,就是在操作系统的管理下,所有正在运行的进程轮流使用CPU,每个进程允许占用CPU的时间非常短(比如10毫秒),这样用户根本感觉不出来CPU是在轮流为多个进程服务,就好象所有的进程都在不间断地运行一样。
  • 并行:同一时刻同时做多件事情,如果有多台CPU,进程数若是小于CPU数,则不同的进程可以分配给不同的CPU来运行,这样多个进程就是真正同时进行,若是进程数大于CPU数,仍需要使用并发技术。
2.3、进程的定义和状态
1、定义

​ 进程是系统进行资源分配和调度的基本单位,进程是程序的基本执行实体

​ 创建进程:BOOL ret = CreateProcess(NULL, chCommandLine, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);

​ 关闭进程:CloseHandle(pi.hThread);

2、状态

操作系统之进程(面试总结)_第1张图片

3、进程的同步和互斥

操作系统之进程(面试总结)_第2张图片

2.4、进程的创建、杀死、退出
1、进程的创建

​ 进程结构由以下几个部分组成:代码段、堆栈段、数据段。代码段是静态的二进制代码,多个程序可以共享。实际上在父进程创建子进程之后,父、子进程除了pid外,几乎所有的部分几乎一样。父、子进程共享全部数据,但并不是说他们就是对同一块数据进行操作,子进程在读写数据时会通过写时复制机制将公共的数据重新拷贝一份,之后在拷贝出的数据上进行操作。如果子进程想要运行自己的代码段,还可以通过调用execv()函数重新加载新的代码段,之后就和父进程独立开了。我们在shell中执行程序就是通过shell进程先fork()一个子进程再通过execv()重新加载新的代码段的过程。

pid_fork(void);//出错返回-1,父进程中返回pid > 0,子进程pid = 0;
2、杀死进程的方式
//用ps查看进程
ps -ef 或 ps -aux;
//杀死进程
kill -s 9 PID;

//进阶版1
ps -ef | grep firefox;
kill -s 9 PID;

//进阶版2
//pgrep的p表明了这个命令是专门用于进程查询的grep
pgrep firefox;//得pid
kill -s 9 PID;

//进阶版3
//和pgrep相比稍显不足的是,pidof必须给出进程的全名
pidof firefox-bin;//得pid
kiss -s 9 PID;

//进阶版4
/*“grep firefox”的输出结果是,所有含有关键字“firefox”的进程。
“grep -v grep”是在列出的进程中去除含有关键字“grep”的进程。
“cut -c 9-15”是截取输入行的第9个字符到第15个字符,而这正好是进程号PID。
“xargs kill -s 9”中的xargs命令是用来把前面命令的输出结果(PID)作为“kill -s 9”命令的参数,并执行该命令。
“kill -s 9”会强行杀掉指定进程。*/
ps -ef | grep firefox | grep -v grep | cut -c 9-15 | xargs kill -s 9;

//进阶版5
pgrep firefox | xargs kill -s 9;
ps -ef | grep firefox | awk '{print $2}' | xargs kill -9;

//进阶版6
pkill -9 firefox;//pkill与kill在这点的差别是:pkill无须 “s”,终止信号等级直接跟在 “-“ 后面。之前我一直以为是 "-s 9",结果每次运行都无法终止进程。
3、进程的退出
1)进程终止的几种方式
  • main函数的自然返回,return
  • 调用exit函数,属于c的函数库
  • 调用_exit函数,属于系统调用
  • 调用abort函数,异常程序终止,同时发送SIGABRT信号给调用进程。
  • 接受能导致进程终止的信号:ctrl+c (^C)、SIGINT(SIGINT中断进程)
2)区别

操作系统之进程(面试总结)_第3张图片

  • exit()和_exit()区别:exit()是对_exit()的封装,都会终止进程并做相关收尾工作,最主要的区别是_exit()函数关闭全部描述符和清理函数后不会刷新流,但是exit()会在调用_'exit()函数前刷新数据流。
  • return和exit()区别:exit()是函数,但有参数,执行完之后控制权交给系统。return若是在调用函数中,执行完之后控制权交给调用进程,若是在main函数中,控制权交给系统。
  • 异常退出方式:abort()、终止信号。
4、如何让进程在后台运行
  • 命令后面加上&即可,实际上,这样是将命令放入到一个作业队列中了
  • ctrl + z 挂起进程,使用jobs查看序号,在使用bg %序号后台运行进程
  • nohup + &,将标准输出和标准错误缺省会被重定向到 nohup.out 文件中,忽略所有挂断(SIGHUP)信号
  • 运行指令前面 + setsid,使其父进程编程init进程,不受HUP信号的影响
  • 将 命令+ &放在()括号中,也可以是进程不受HUP信号的影响
5、一个进程可以创建多少个线程?

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

2.5、进程间通信
  • 信号量(semophore):计数器,控制多个进程对共享资源的访问,做为一种锁的机制,防止某进程在访问共享资源的时候,其他进程也在访问该资源。主要作为进程间以及同一进程内不同线程之间的同步手段。(信号量的数据结构为一个值及一个指针,指针指向等待该信号量的下一个进程)。
  • 信号(sinal、notify):一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;
  • 共享内存(shared region):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,多个线程可以访问。往往与notify、semophere配合使用,来实现进程间的同步和通信。多个进程可以同时操作,所以需要进行同步。信号量用来同步对共享内存的访问。
  • 套接字(socket):用于不同器件间的进程通信。可以将套接字看作不同主机间的进程进行双间通信的端点。
  • 消息队列(message queue):消息队列是消息链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递量少、管道只能承载无格式字节流以及缓冲区大小受限等缺点;
  • 管道(pipe):管道是半双工通信方式,数据只能单向流动,且只能在具有亲缘关系间的进程使用。进程的亲缘关系通常是指父子进程关系;
    • 有名管道(FIFO文件,借助文件系统):半双工通信方式,但允许无亲缘关系进程间通信。
    • 无名管道(内存文件):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常指父子进程关系。
2.6、进程上下文切换

​ 进程切换:切换虚拟地址空间、切换内核栈、硬件上下文

​ 线程切换:切换内核栈、硬件上下文。

进程的上下文切换:不仅包含了虚拟内存、栈、全局变量等⽤户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。

  • 切换页目录以使用新的地址空间

  • 切换内核栈和硬件上下文

对于linux来说,线程和进程的最大区别就在于地址空间,对于线程切换,第1步是不需要做的,第2是进程和线程切换都要做的。

1、进程上下文切换开销
1)切换的性能消耗:
  • 线程上下文切换和进程上下文切换一个最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。
  • 另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲(processor’s Translation Lookaside Buffer (TLB))被全部刷新,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题。
2)切换的开销:
  • 最显著的性能损耗是将保存寄存器中的内容
  • CPU高速缓存失效
    • 页表查找是一个很慢的过程,因此通常使用Cache来缓存常用的地址映射,这样可以加速页表查找,这个cache就是TLB。当进程切换后页表也要进行切换,页表切换后TLB就失效了,cache失效导致命中率降低,那么虚拟地址转换为物理地址就会变慢,表现出来的就是程序运行会变慢。
2、线程上下文切换的原因?

​ 引入线程(线程上下文切换开销较小)。

  • 当前执行任务的时间用完之后,系统CPU正常调度下一个任务;(时间片用完)
  • 当前执行任务碰到I/O阻塞,调度器将此任务挂起,继续下一个任务;(I/O阻塞)
  • 多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一个任务;(没抢到资源)
  • 用户代码挂起当前任务,让出CPU时间;(挂起)
  • 硬件中断。(中断)
3、用 vmstat 查看上下文切换次数
4、如何减少上下文切换

​ 既然上下文切换会导致额外的开销,因此减少上下文切换次数便可以提高多线程程序的运行效率。减少上下文切换的方法有:无锁并发编程、CAS算法、使用最少的线程、使用协程。

  • 无锁并发编程:多线程竞争时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,比如将数据的ID按照hash取模分段,不同的线程处理不同段的数据;
  • CAS算法:利用CAS算法来更新数据,不需要进行加锁处理;
  • 使用最少的线程:避免创建不需要的线程,比如任务很少,但是常见了很多线程来处理,这就导致了大量线程处于等待状态;
  • 使用协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
2.7、详解进程上下文切换

​ 原文链接:https://blog.csdn.net/21cnbao/article/details/108860584

1、进程上下文的概念

​ 进程上下文是进程执行活动全过程的静态描述。我们把已执行过的进程指令和数据在相关寄存器与堆栈中的内容称为进程上文,把正在执行的指令和数据在寄存器与堆栈中的内容称为进程正文,把待执行的指令和数据在寄存器与堆栈中的内容称为进程下文。

​ 实际上linux内核中,进程上下文包括进程的虚拟地址空间硬件上下文。进程硬件上下文包含了当前cpu的一组寄存器的集合,arm64中使用task_struct结构的thread成员的cpu_context成员来描述,包括x19-x28,sp, pc等。

操作系统之进程(面试总结)_第4张图片

2、上下文切换过程

​ 进程上下文切换主要涉及到两部分主要过程:进程地址空间切换处理器状态切换。地址空间切换主要是针对用户进程而言,而处理器状态切换对应于所有的调度单位。

1)进程地址空间切换

​ 进程地址空间指的是进程所拥有的虚拟地址空间,而这个地址空间是假的,是Linux内核通过数据结构来描述出来的,从而使得每一个进程都感觉到自己拥有整个内存的假象,CPU访问的指令和数据最终会落实到实际的物理地址,对用进程而言通过缺页异常来分配和建立页表映射。进程地址空间内有进程运行的指令和数据,因此到调度器从其他进程重新切换到我的时候,为了保证当前进程访问的虚拟地址是自己的必须切换地址空间。
​ 实际上,进程地址空间使用mm_struct结构体来描述,这个结构体被嵌入到进程描述符(我们通常所说的进程控制块PCB)task_struct中,mm_struct结构体将各个vma组织起来进行管理,其中有一个成员pgd至关重要,地址空间切换中最重要的是pgd的设置。

pgd中保存的是进程的页全局目录的虚拟地址,记住保存的是虚拟地址,那么pgd的值是何时被设置的呢?答案是fork的时候,如果是创建进程,需要分配设置mm_struct,其中会分配进程页全局目录所在的页,然后将首地址赋值给pgd。

​ 进程地址空间转换最终将进程的pgd虚拟地址转化为物理地址存放在(ttbr0_el1)用户空间的页表基址寄存器,当访问用户空间地址的时候MMU会通过这个寄存器来做遍历页表获得物理地址(ttbr1_el1是内核空间的页表基址寄存器,访问内核空间地址时使用,所有进程共享,不需要切换)。完成了这一步,也就完成了进程的地址空间切换,确切的说是进程的虚拟地址空间切换。

:上述仅仅切换用户地址空间,内核地址空间由于是共享的不需要切换,也就是为何切换到内核线程不需要也没有地址空间的原因。

2)处理器状态(硬件上下文)切换

​ 地址空间切换,只是保证了进程访问指令数据时访问的是自己地址空间(当然上下文切换的时候处于内核空间,执行的是内核地址数据,当返回用户空间的时候才有机会执行用户空间指令数据**,地址空间切换为进程访问自己用户空间做好了准备**),但是进程执行的内核栈还是前一个进程的,当前执行流也还是前一个进程的,需要做切换。

​ 实际上,处理器状态切换就是将前一个进程的sp,pc等寄存器的值保存到一块内存上,然后将即将执行的进程的sp,pc等寄存器的值从另一块内存中恢复到相应寄存器中,恢复sp完成了进程内核栈的切换,恢复pc完成了指令执行流的切换。其中保存/恢复所用到的那块内存需要被进程所标识,这块内存这就是cpu_contex这个结构的位置(进程切换都是在内核空间完成)。

​ 由于用户空间通过异常/中断进入内核空间的时候都需要保存现场,也就是保存发生异常/中断时的所有通用寄存器的值,内核会把“现场”保存到每个进程特有的进程内核栈中,并用pt_regs结构来描述,当异常/中断处理完成之后会返回用户空间,返回之前会恢复之前保存的“现场”,用户程序继续执行。
​ 所以当进程切换的时候,当前进程被时钟中断打断,将发生中断时的现场保存到进程内核栈(如:sp, lr等),然后会切换到下一个进程,当再次回切换回来的时候,返回用户空间的时候会恢复之前的现场,进程就可以继续执行(执行之前被中断打断的下一条指令,继续使用自己用户态sp),这对于用户进程来说是透明的。

3、ASID机制

​ ASID(Address Space Identifer 地址空间标识符)。

4、普通用户进程、普通用户线程、内核线程切换的差别
5、进程切换全景图
总结进程和线程区别
  • 线程是运⾏在进程中的⼀个“逻辑流”,单进程中可以运⾏多个线程,同进程⾥的线程可以共享进程的部分资源的,⽐如⽂件描述符列表、进程空间、代码、全局数据、堆、共享库等,这些共享些资源在上下⽂切换时是不需要切换,⽽只需要切换线程的私有数据、寄存器等不共享的数据,因此同⼀个进程下的线程上下⽂切换的开销要⽐进程小得多。
  • 一个进程挂掉了不会影响其他进程,而线程挂掉了会影响其他线程
2.8、僵尸进程
1、什么是僵尸进程?

​ 如果子进程先退出,父进程还没退出,那么子进程必须等到父进程捕获到了子进程的退出状态才真正结束,否则这个时候子进程就成为僵尸进程。

2、设置僵尸进程的目

​ 设置僵尸进程的目的是维护子进程的信息,以便父进程在以后某个时候获取。这些信息至少包括进程ID,进程的终止状态,以及该进程使用的CPU时间,所以当终止子进程的父进程调用wait或waitpid时就可以得到这些信息。如果一个进程终止,而该进程有子进程处于僵尸状态,那么它的所有僵尸子进程的父进程ID将被重置为1(init进程)。继承这些子进程的init进程将清理它们(也就是说init进程将wait它们,从而去除它们的僵尸状态)。

3、如何避免僵尸进程?
  • 通过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进程去处理,省去了大量僵尸进程占用系统资源。

2.9、孤儿进程

​ 如果父进程先退出,子进程还没退出,那么子进程的父进程将变为init进程。(注:任何一个进程都必须有父进程)。一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

2.10、守护进程
1、什么是守护进程?

​ 指在后台运行的,没有控制终端与之相连的进程。它独立于控制终端,周期性地执行某种任务。Linux的大多数服务器就是用守护进程的方式实现的,如web服务器进程http等。

2、创建守护进程要点
  • 让程序在后台执行。方法是调用fork()产生一个子进程,然后使父进程退出。
  • 调用setsid()创建一个新对话期。控制终端、登录会话和进程组通常是从父进程继承下来的,守护进程要摆脱它们,不受它们的影响,方法是调用setsid()使进程成为一个会话组长。setsid()调用成功后,进程成为新的会话组长和进程组长,并与原来的登录会话、进程组和控制终端脱离。
  • 禁止进程重新打开控制终端。经过以上步骤,进程已经成为一个无终端的会话组长,但是它可以重新申请打开一个终端。为了避免这种情况发生,可以通过使进程不再是会话组长来实现。再一次通过fork()创建新的子进程,使调用fork的进程退出。
  • 关闭不再需要的文件描述符。子进程从父进程继承打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。首先获得最高文件描述符值,然后用一个循环程序,关闭0到最高文件描述符值的所有文件描述符。
  • 将当前目录更改为根目录。
  • 子进程从父进程继承的文件创建屏蔽字可能会拒绝某些许可权。为防止这一点,使用unmask(0)将屏蔽字清零。
  • 处理SIGCHLD信号。对于服务器进程,在请求到来时往往生成子进程处理请求。如果子进程等待父进程捕获状态,则子进程将成为僵尸进程(zombie),从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将SIGCHLD信号的操作设为SIG_IGN。这样,子进程结束时不会产生僵尸进程。
2.11、进程同步的方法
1、临界区

​ 对临界资源进行访问的那段代码称为临界区。为了互斥访问临界资源,每个进程在进入临界区之前,需要先进行检查。

2、同步和互斥
  • 同步:多个进程因为合作产生的直接制约关系,使得进程有一定的先后执行关系。
  • 互斥:多个进程在同一时刻只有一个进程能进入临界区。
3、信号量

​ 信号量(Semaphore)是一个整型变量,可以对其执行 down 和 up 操作,也就是常见的 P 和 V 操作。

  • down : 如果信号量大于 0 ,执行 -1 操作;如果信号量等于 0,进程睡眠,等待信号量大于 0;
  • up :对信号量执行 +1 操作,唤醒睡眠的进程让其完成 down 操作。

​ down 和 up 操作需要被设计成原语,不可分割,通常的做法是在执行这些操作的时候屏蔽中断。如果信号量的取值只能为 0 或者 1,那么就成为了 互斥量(Mutex) ,0 表示临界区已经加锁,1 表示临界区解锁。

typedef int semaphore;
semaphore mutex = 1;
void P1() {
    down(&mutex);
    // 临界区
    up(&mutex);
}
void P2() {
    down(&mutex);
    // 临界区
    up(&mutex);
}
4、管程

​ 管程有一个重要特性:在一个时刻只能有一个进程使用管程。进程在无法继续执行的时候不能一直占用管程,否则其它进程永远不能使用管程。管程引入了 条件变量 以及相关的操作:wait()signal() 来实现同步操作。对条件变量执行 wait() 操作会导致调用进程阻塞,把管程让出来给另一个进程持有。signal() 操作用于唤醒被阻塞的进程。

2.12、父进程、子进程、进程组、作业、会话
1、父进程

​ 已创建一个或多个子进程的进程。

2、子进程

​ 由fork创建的新进程被称为子进程(child process)。该函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是新进程(子进程)的进程 id。将子进程id返回给父进程的理由是:

  • 因为一个进程的子进程可以多于一个,没有一个函数使一个进程可以获得其所有子进程的进程id。对子进程来说,之所以fork返回0给它,是因为它随时可以调用getpid()来获取自己的pid;
  • 也可以调用getppid()来获取父进程的id。(进程id 0总是由交换进程使用,所以一个子进程的进程id不可能为0 )。

​ fork之后,操作系统会复制一个与父进程完全相同的子进程,虽说是父子关系,但是在操作系统看来,他们更像兄弟关系,这2个进程共享代码空间,但是数据空间是互相独立的,子进程数据空间中的内容是父进程的完整拷贝,指令指针也完全相同,子进程拥有父进程当前运行到的位置(两进程的程序计数器pc值相同,也就是说,子进程是从fork返回处开始执行的),但有一点不同,如果fork成功,子进程中fork的返回值是0,父进程中fork的返回值是子进程的进程号,如果fork不成功,父进程会返回错误。

​ 子进程与父进程:

  • 继承:进程的资格(真实(real)/有效(effective)/已保存(saved)用户号(UIDs)和组号(GIDs))、环境(environment)、堆栈、内存、进程组号;
  • 独有:进程号、不同的父进程号(即子进程的父进程号与父进程的父进程号不同, 父进程号可由getppid函数得到)、资源使用(resource utilizations)设定为0。
3、进程组

​ 进程组就是多个进程的集合,其中肯定有一个组长,其进程PID等于进程组的PGID。只要在某个进程组中一个进程存在,该进程组就存在,这与其组长进程是否终止无关。

4、作业

​ shell分前后台来控制的不是进程而是作业(job)或者进程组(Process Group)。一个前台作业可以由多个进程组成,一个后台也可以由多个进程组成,shell可以运行一个前台作业和任意多个后台作业,这称为作业控制。

1)为什么只能运行一个前台作业?

​ 当我们在前台新起了一个作业,shell就被提到了后台,因此shell就没有办法再继续接受我们的指令并且解析运行了。 但是如果前台进程退出了,shell就会有被提到前台来,就可以继续接受我们的命令并且解析运行。

2)作业与进程组的区别

​ 如果作业中的某个进程有创建了子进程,则该子进程是不属于该作业的。一旦作业运行结束,shell就把自己提到前台(子进程还存在,但是子进程不属于作业),如果原来的前台进程还存在(这个子进程还没有终止),他将自动变为后台进程组。

5、会话

​ 会话(Session)是一个或多个进程组的集合。一个会话可以有一个控制终端。在xshell或者WinSCP中打开一个窗口就是新建一个会话。

你可能感兴趣的:(操作系统,面试,职场和发展)