聊聊Linux下fork与线程池

前言

这个知识点也是老生常谈了,咱们今天就把他讲清楚弄明白。

线程池

线程池是指在一个多线程程序中创建一个线程集合,在执行新的任务的时候不是新建一个线程,而是使用线程池中已创建好的线程,一旦任务执行完毕,线程就会休眠等待新的任务分配下来。
这么看来,线程池的优点就很明显了,在频繁的线程切换环境中,线程池可以省去多次线程的创建与释放所需要的时间。

Linux下的线程是如何创建的?

这个我还是稍微了解一点的。创建一个线程非常简单。
1.向内核空间申请一段内存用来存放新线程的PCB;
2.PCB中预留指针指向内核栈;
3.PCB中存放待执行线程函数与函数参数;
4.加入进程调度队列或直接ret执行(需要先将函数指针放在栈顶,然后ret会将其加载到eip寄存器上,完成立刻执行线程操作)

fork

简言之,fork就是创建了一个父进程的拷贝。
使用fork函数得到的子进程从父进程的继承了整个进程的地址空间,包括:进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设置、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端等。
子进程与父进程的区别在于:
1、父进程设置的锁,子进程不继承(因为如果是排它锁,被继承的话,矛盾了)
2、各自的进程ID和父进程ID不同
记住,fork是一次调用两次返回值,因为会在两个地址空间中都执行返回。
如果成功创建一个子进程,对于父进程来说返回子进程ID
如果成功创建一个子进程,对于子进程来说返回值为0
如果为-1表示创建失败。
fork系统调用复制产生的子进程与父进程(调用进程)基本一样:代码段+数据段+堆栈段+PCB,当前的运行环境基本一样,所以子进程在fork之后开始向下执行,而不会从头开始执行。

孤儿进程与僵尸进程

fork系统调用之后,父子进程将交替执行,执行顺序不定。
如果父进程先退出,子进程还没退出那么子进程的父进程将变为init进程(托孤给了init进程)。(注:任何一个进程都必须有父进程)
如果子进程先退出,父进程还没退出,那么子进程必须等到父进程捕获到了子进程的退出状态才真正结束,否则这个时候子进程就成为僵进程(僵尸进程:只保留一些退出信息供父进程查询)。僵尸进程可以在进程列表中查看。

Linux如何查看进程状态呢?

ps指令了解一下?process state。多的咱就不说了,百度上多得是介绍。

孤儿进程与僵尸进程的危害

首先任何进程都有父进程(init进程是所有进程的祖宗,它没有爸爸),当父进程抛弃了自己fork出来的子进程而执行完成后,子进程就被托管给init进程。
孤儿进程其实没啥危害。
僵尸进程就不一样了。本来吧,父进程在fork一个子进程的时候,接下来需要wait操作等待子进程结束,然后子进程才会全部释放资源(子进程执行完成后并没有全部释放资源,还剩下PID这种东西,退出状态这种东西,必须要父进程wait索取才会释放)。这下好了,父进程直接没wait就退出了,于是僵尸进城产生后,这个进程ID就再也不能被后续使用了。

如何解决僵尸进程

很简单,第一个是signal(可能有的朋友不了解什么是signal)。Linux的信号机制就提供了其中一种专门用来处理子进程退出问题的,叫SIGCHLD,写法也非常简单:

#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);
}

是不是非常简单?
再给出第二种解决方法,让父子进程摆脱父子关系,于是子进程的父亲变成了init进程,这个进程肯定能帮咱们处理僵尸进程的问题。这确实是一种非常棒的思路,咱们聊聊具体的实现方法:父进程fork一个子进程,然后子进程里面再fork一个孙子进程。这个时候突然子进程创建完成后就退出,那么,孙子进程就没有爸爸了(祖安程序员)。不能说孙子进程没有爸爸了,而是说,孙子进程的爸爸变为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;
}

copy on write

写时复制就很能理解。各位朋友看看,fork一个进程可是真的浪费了好多资源啊,啥啥都得复制一份,太浪费了吧。那能不能咱们搞点棒棒的机制,解决一下这个问题?
写入时复制(Copy-on-write)是一个被使用在程式设计领域的最佳化策略。其基础的观念是,如果有多个呼叫者(callers)同时要求相同资源,他们会共同取得相同的指标指向相同的资源,直到某个呼叫者(caller)尝试修改资源时,系统才会真正复制一个副本(private copy)给该呼叫者,以避免被修改的资源被直接察觉到,这过程对其他的呼叫只都是通透的(transparently)。此作法主要的优点是如果呼叫者并没有修改该资源,就不会有副本(private copy)被建立。

vfork

vfork就很省事情,它不会复制一份新的分页表,而是依旧指向父进程的分页表,那么你俩可不就是共享地址空间了嘛!那不就是你俩共享物理空间了嘛!那肯定不行啊,这样搞的话岂不是要乱了?
于是vfork有了这样一个规则:由vfork创造出来的子进程还会导致父进程挂起,除非子进程exit或者execve才会唤起父进程。这样一来,不就没那么多烦恼了。

vfork return带来的混乱,必须用exit

vfork命好啊,一定会先于父进程执行。那么当vfork进程执行完成后,能用return退出么?
答案肯定是不能!这其实非常好理解。return会释放掉栈里面的局部变量,exit不会。本来父子进程是共享内存地址的,对于栈里面的东西,最好还是vfork刚放进去的就拿出来,换言之,自己玩自己放进去的东西。vfork如果return,那么栈直接就被糟蹋了。

为什么会有vfork这种鬼东西?

事出必有因。
咱们先了解一下exec调用。
系统调用exec是以新的进程去代替原来的进程,但进程的PID保持不变。因此,可以这样认为,exec系统调用并没有创建新的进程,只是替换了原来进程上下文的内容。原进程的代码段,数据段,堆栈段被新的进程所代替。
咱们想想啊,exec一般是在fork进程中执行,本来就要将fork子进程把父进程替换掉,那咱们fork还把地址空间的东西费劲巴拉的拷贝一份干嘛呢?
一句话,vfork是给exec用的。

最棒的clone

这个咱们就先不深入讲解。
clone可以让你有选择性的继承父进程的资源,你可以选择想vfork一样和父进程共享一个虚存空间,从而使创造的是线程,你也可以不和父进程共享,你甚至可以选择创造出来的进程和父进程不再是父子关系,而是兄弟关系。

猜你喜欢

转载自blog.csdn.net/weixin_44039270/article/details/106736154