进程以及和线程的区别(一)

一 进程和线程的区别

  进程:在系统中能独立运行并作为资源分配的基本单位,它是由一组机器指令、数据和堆栈等组成的,是一个能独立运行的活动实体。进程有狭义和广义之分,狭义的进程是正在运行的程序的实例;广义的进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动,是操作系统动态执行的基本单元。
  线程:线程是进程中的一个实体,作为系统调度和分派的基本单位。Linux下的线程看作轻量级进程(LWP)。一个标准的线程由线程ID、当前指令指针(PC)、寄存器集合和堆栈组成。
  (1)调度:进程是资源的分配和调度的一个独立单元,而线程是CPU调度的基本单元。
  (2)资源:进程都是拥有系统资源的一个独立单位,他可以拥有自己的资源;同一个进程中可以包括多个线程,并且线程共享整个进程的资源(寄存器、堆栈、上下文),线程中执行时一般都要进行同步和互斥,一个进程至少包括一个线程。
  (3)生命周期:进程结束后它拥有的所有线程都将销毁,而线程的结束不会影响同个进程中的其他线程的结束。但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。
  (4)系统开销:线程是轻两级的进程,它的创建和销毁所需要的时间比进程小很多,所有操作系统中的执行功能都是创建线程去完成的。
  (5)通信:线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。
  (6)线程有自己的私有属性TCB,线程id,寄存器、硬件上下文,而进程也有自己的私有属性进程控制块PCB,这些私有属性是不被共享的,用来标示一个进程或一个线程的标志。

二 进程

  进程四要素:(1)有一段程序供其执行(不一定是一个进程所专有的),就像一场戏必须有自己的剧本;(2)有自己的专用系统堆栈空间(私有财产);(3)有进程控制块(task_struct)(“有身份证,PID”);(4)有独立的存储空间。
  缺少第四条的称为线程,如果完全没有用户空间称为内核线程,共享用户空间的称为用户线程。
  进程ID:每一个进程都由一个唯一的标识符表示,即进程ID,简称pid。系统保证在每一个时刻pid都是唯一的。空闲进程(idle process)–没有其他进程运行时,内核运行的进程,pid为0,在启动后,内核运行的第一个进程为init进程,pid为1。如果不指定,则寻找。每个进程的父进程ppid,每个进程都被一个用户和组所拥有。每个进程都是某一个进程组的一部分,简单的表明了和其他进程之间的关系。缺省最大值为32768,保持兼容。
  用户和组:每个子进程都继承了父进程的用户和组。getpid()函数返回调用进程id,getppid()返回调用进程父进程id。在Unix中,载入内存并运行一个进程映像和和创建一个新进程的操作是分离的。exec()系列函数是可以将一个二进制程序映像载入内存替换原先进程的地址空间,并可是运行。
  写时复制(写时拷贝):是一种采取了惰性优化方案来避免复制时的系统开销。即多个进程进程读取它们自己的那部分资源,就不必要去复制,只要保存一个指向资源的指针,只要当修改时才去复制。在使用虚拟内存的情况下,写时复制是以页为基础进行的。写时复制在内核中的实现非常简单。与内核页相关的数据结构可以被标记为只读和写时复制。如果有进程试图修改一个页就会产生一个缺页中断。内核处理缺页中断处理的方式就是对该页进行一次透明复制。这时会清除页面的 CoW 属性,表示着它不再被共享。现代的计算机结构体系中都在内存管理单元( MMU )提供了硬件级别的写时复制支持,所以实现是很容易的。
  僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。
  当一个子进程终止时,内核会向父进程发送SIGCHILD信号。SIGCHILD信号可能会在任何时候产生,也会在任何时候发送给父进程。很多的父进程想知道子进程终止的更多信息,例如返回值。Unix设计:当一个子进程在父进程之前结束,内核把子进程设置成特殊状态,叫僵尸进程。僵尸进程只保留最小的概要信息–一些保存着有用信息的内核数据结构,僵尸进程等待父进程来查询自己的信息,只要父进程获取了信息,僵尸进程就结束。wait()等待子进程的结束。如果父进程在子进程结束之前就结束了,则无论何时,内核就会遍历它所有的子进程,并且把它们的父进程设置成init进程。

//利用system函数执行shell脚本, 发现进程状态为Z的僵尸进程.
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

#define MAXLINE 80

void if_error(int stat_code, char *err_msg)
{
    if (stat_code < 0) {
        perror(err_msg);
        exit(errno);
    }
}

int main(int argc, char **argv)
{
    pid_t chi_pid;
    char shell_cmd[MAXLINE];

    chi_pid = fork();
    if_error(chi_pid, "fork");

    if (chi_pid == 0) {
    /* child process */
        exit(0);
    } else {
    /* parent process */
        sleep(2);
    }

    sprintf(shell_cmd, "ps aux | grep %d", chi_pid);
    system(shell_cmd);

    printf("main exit.\n");
    return 0;
}

  孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
  问题及危害
  unix提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息, 就可以得到。这种机制就是: 在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)。直到父进程通过wait / waitpid来取时才释放。 但这样就导致了问题,如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。
  孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了init进程身上,init进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤 儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。
  任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个子进程在结束时都要经过的阶段。如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时 处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。
  僵尸进程解决办法
  (1)通过信号机制:
  子进程退出时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号。在信号处理函数中调用wait进行处理僵尸进程。测试程序如下所示:

#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <signal.h>

static void sig_child(int signo);

int main()
{
    pid_t pid;
    //创建捕捉子进程退出信号
    signal(SIGCHLD,sig_child);
    pid = fork();
    if (pid < 0)
    {
        perror("fork error:");
        exit(1);
    }
    else if (pid == 0)
    {
        printf("I am child process,pid id %d.I am exiting.\n",getpid());
        exit(0);
    }
    printf("I am father process.I will sleep two seconds\n");
    //等待子进程先退出
    sleep(2);
    //输出进程信息
    system("ps -o pid,ppid,state,tty,command");
    printf("father process is exiting.\n");
    return 0;
}

static void sig_child(int signo)
{
     pid_t        pid;
     int        stat;
     //处理僵尸进程
     while ((pid = waitpid(-1, &stat, WNOHANG)) >0)
            printf("child %d terminated.\n", pid);
}

  (2)fork两次
  《Unix 环境高级编程》8.6节说的非常详细。原理是将子进程成为孤儿进程,从而其的父进程变为init进程,通过init进程可以处理僵尸进程。测试程序如下所示:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

int main()
{
    pid_t  pid;
    //创建第一个子进程
    pid = fork();
    if (pid < 0)
    {
        perror("fork error:");
        exit(1);
    }
    //第一个子进程
    else if (pid == 0)
    {
        //子进程再创建子进程
        printf("I am the first child process.pid:%d\tppid:%d\n",getpid(),getppid());
        pid = fork();
        if (pid < 0)
        {
            perror("fork error:");
            exit(1);
        }
        //第一个子进程退出
        else if (pid >0)
        {
            printf("first procee is exited.\n");
            exit(0);
        }
        //第二个子进程
        //睡眠3s保证第一个子进程退出,这样第二个子进程的父亲就是init进程里
        sleep(3);
        printf("I am the second child process.pid: %d\tppid:%d\n",getpid(),getppid());
        exit(0);
    }
    //父进程处理第一个子进程退出
    if (waitpid(pid, NULL, 0) != pid)
    {
        perror("waitepid error:");
        exit(1);
    }
    exit(0);
    return 0;
}

  (3) 如果父进程不关心子进程什么时候结束,那么可以用signal(SIGCLD, SIG_IGN)或signal(SIGCHLD, SIG_IGN)通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收,并不再给父进程发送信号。
  (4) 父进程通过wait和waitpid等函数等待子进程结束,这会导致父进程挂起。

三 守护进程

  参考:
  Linux守护进程编写:https://blog.csdn.net/zg_hover/article/details/2553321
  Linux–进程组、会话、守护进程:http://www.cnblogs.com/forstudy/archive/2012/04/03/2427683.html
  Linux进程间通信—创建守护进程
https://blog.csdn.net/y396397735/article/details/50589314

  临界资源:系统中某些资源一次只允许一个进程使用,这样的资源称为临界资源或互斥资源或共享资源。
  临界区:各个进程中对某个临界资源实施操作的程序片段。
  临界区使用原则
  1. 没有进程在临界区时,想进入临界区的进程可进入。
  2. 不允许两个进程同时在临界区。
  3. 临界区运行的进程不得阻塞其他进程进入临界区。
  4. 不得使进程无限等待进入临界区。

四 死锁状态

  死锁:死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
  产生死锁的原因
  (1) 因为系统资源不足
  (2) 进程运行推进的顺序不合适
  (3) 资源分配不对等
  产生死锁的四个必要条件
  (1) 互斥条件,一个资源每次只能被一个进程使用,如果有另一个进程申请该资源,那么申请进程必须等到该资源被释放为止。
  (2) 请求与保持条件(占有并等待),一个进程必须占有至少一个资源,并等待另一为其他进程所占有的资源。
  (3) 不剥夺条件(非抢占)),进程已获得的资源,在未使用完之前,不能强行剥夺,只能在进程完成任务和自动释放。
  (4) 循环等待条件,若干进程之间形成的一种头尾相接的循环等待资源关系,例如有一组等待进程{p0,p1,p2,p3,p4}, p0 等待的资源为p1所占有,p1 等待的资源为p2所占有,p2等待的资源为p3所占有,p3 等待的资源为p4所占有,而p4 等待的资源为P1 所占有。

有5个可用的某类资源,由四个进程共享,每个进程最多可申请()个资源,使系统不死锁?
设临界资源为m, 共享进程为n个,每个进程最多可以申请x个资源,则当m>n 时,m>n(x-1) 使系统不死锁,当m<n 时,x=1;
因此x=2;

  解决死锁的基本方法
  (1)预防死锁:
  资源一次性分配:(破坏请求和保持条件);
  可剥夺资源:即当某进程新的资源未满足时,释放已占有的资源(破坏不可剥夺条件);
  资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)。
  (2)避免死锁:
  预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得 较满意的系统性能。由于在避免死锁的策略中,允许进程动态地申请资源。因而,系统在进行资源分配之前预先计算资源分配的安全性。若此次分配不会导致系统进入不安全状态,则将资源分配给进程;否则,进程等待。其中最具有代表性的避免死锁算法是银行家算法。
  (3)检测死锁:
  首先为每个进程和每个资源指定一个唯一的号码;然后建立资源分配表和进程等待表。
  (4)解除死锁:
  当发现有进程死锁后,便应立即把它从死锁状态中解脱出来,常采用的方法有:剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。

五 活锁

  活锁指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试—失败—尝试—失败的过程。处于活锁的实体是在不断的改变状态,活锁有可能自行解开。

发布了67 篇原创文章 · 获赞 26 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/essity/article/details/84962958