Skip to content

Latest commit

 

History

History
75 lines (46 loc) · 6.26 KB

processandthread.md

File metadata and controls

75 lines (46 loc) · 6.26 KB

进程与线程

进程

  1. 进程是操作系统内存分配的基本单位,因此进程拥有一个独立的内存空间和寄存器组,而且一个进程崩溃不会影响其它进程。
  2. 进程间通信(IPC)的方式通常有: 管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。
  3. 进程至少有 5 种基本状态,它们是:初始态,执行态,等待状态,就绪状态,终止状态。
  4. 一个没有线程的进程可以被看作是单线程的。

线程

  1. 线程是 CPU 调度的基本单位,线程上下文切换比进程上下文切换要快得多。线程必须依赖于进程而存在,但是线程还是有自己的栈和栈指针,程序计数器等寄存器。
  2. 线程间的通信:直接读写进程数据段(如全局变量)来进行通信,需要通过 进程同步 和 互斥量 的手段来保证在并发条件下的数据一致性的问题。
  3. 线程有 3 个基本状态:就绪,执行,阻塞。 线程存在 5 种基本操作来切换线程的状态:派生,阻塞,激活,调度,结束。

进程创建 fork

fork()函数一次调用两次返回,对于子进程它返回0,对于父进程它返回子进程的pid。 fork()在子进程需要装载父进程的内存的拷贝时 触发 内存分配,系统会为其分配和父进程相同的内存空间。 fork()还会复制父进程的文件描述符,exec也会保留调用它的进程的文件描述符。fork()不会复制文件描述符偏移,因此文件描述符偏移在父子进程之间是共享的(这一点和dup也是一样的:只复制文件描述符而不复制文件描述符偏移)。

所以就有了IO重定向的思路: fork一个进程,子进程关闭标准输入文件描述符,子进程打开目的IO,exec替换子进程 这样 exec运行的进程 就关联了 替换了 标准输入输出 的 目的IO。

子进程与父进程的退出

因为父进程创建完子进程后不清楚到底是谁先退出,一般情况下是父进程先退出,然后父进程使用wait_pid等待子进程的退出 然而 如果 父进程先退出并且没有等待子进程退出,它自己就退出了就会使得子进程变成 孤儿进程。 如果子进程先退出,并且没有任何信号通知父进程自己已经退出了就会使得子进程的状态信息残留,而被称为 僵尸进程。

孤儿进程

孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

没有父进程的孤儿进程会被0号进程收养,因此孤儿进程是无害的。

僵尸进程

僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。

如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。

僵尸进程的解决方法:

  1. 通过信号机制。子进程退出时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号。在信号处理函数中调用wait进行处理僵尸进程。
  2. fork两次。 将子进程成为孤儿进程,从而其的父进程变为init进程,通过init进程可以处理僵尸进程。

守护进程

守护进程是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。

守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机都保持运行。

一个守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,所以它是一个由init继承的孤儿进程。守护进程是非交互式程序,没有控制终端,所以任何输出,无论是向标准输出设备stdout还是标准出错设备stderr的输出都需要特殊处理。

编写守护进程的一般步骤步骤:

  1. 在父进程中执行fork并exit退出父进程; 子进程继承了父进程的进程组ID ,但具有一个新的进程ID ,这就保证了子进程不是一个进程组的首进程。
  2. 在子进程中调用setsid函数创建新的会话; 登录会话可以包含多个进程组,这些进程组共享一个控制终端,这个控制终端通常是创建进程的登录终端、控制终端,登录会话和进程组通常是从父进程继承下来的。我们的目的就是要摆脱它们,使之不受它们的影响。 当进程是会话组长时,setsid()调用会失败,但第一点已经保证进程不是会话组长。setsid()调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离,由于会话过程对控制终端的独占性,进程同时与控制终端脱离。
  3. 再次调用fork函数 需要采用不再让该子进程称为会话首进程的方式来禁止进程重新打开关联的控制终端。再次调用fork函数,该fork函数执行后,子进程结束,那么孙子进程就不再是会话首进程,避免会话再次被关联到控制终端。
  4. 在子进程中调用chdir函数,让根目录 ”/” 成为子进程的工作目录;
  5. 在子进程中调用umask函数,设置进程的umask为0;
  6. 在子进程中关闭任何不需要的文件描述符

管道

管道与普通的IO相比有三个优点: 数据长度任意 自我清扫 允许同步

同步的实现是:两个进程通过管道来进行信息的传递,其中一个进程对管道进行读,然后被阻塞,知道另一个进程对同一对管道进行写的时候才会消除之前进程的阻塞,这就实现了进程间的同步。