fork用于创建进程,使用时需要包含如下头文件:

        #include <sys/types.h>
        #include <unistd.h>

1.进程简介:

        进程是一个向CPU申请资源的正在执行的程序,和线程相比进程之间的资源相互独立,一个进程至少有一个线程。

        进程分为就绪状态,执行状态和阻塞状态。就绪状态指资源已获取完毕,等待cpu调用的状态。执行状态就是正在执行的状态。阻塞状态是进程执行受阻,暂停运行的状态。

        进程健壮性好,但是创建进程和进程切换消耗的资源较多。

        进程创建有两种方式,由操作系统直接创建或者由一个进程创建它的子进程。操作系统直接创建的进程的资源之间一般没有联系,且这些进程是平等的。

        linux查看进程可以用:ps -ef,ps aux这两个命令。

2.进程创建:

        c/c++使用fork创建子进程。fork会在父进程和新的子进程分别返回不同的返回值。父进程的返回值为子进程的ID,子进程的返回值为0。示例如下:

#include <iostream>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    pid_t pid;
    pid = fork();
    //根据pid是否大于0判断是父进程还是子进程
    if (pid > 0) {
        std::cout << "Parent process" << '\n';
        return 0;
    }
    std::cout << "Child process" << '\n';
    return 0;
}

        输出结果为:

cppcheck命令行生成html报告python环境配置 cpp fork_子进程

 

        可以看出父进程和子进程都成功输出。 

3.僵尸进程和孤儿进程:

3.1 孤儿进程:

        孤儿进程指的是父进程执行完毕退出,子进程失去了它的父进程,由init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。孤儿进程虽然不好听,但是它基本上没有危害,而且守护进程的创建还需要孤儿进程。这点后面会提到。

        孤儿进程就是一号进程,ps查看寻找PID为1的就可以了。

3.2 僵尸进程:

        僵尸进程是子进程退出而父进程没有回收子进程资源产生的,这样会导致资源泄漏。僵尸进程会占用资源,是有害的。解决方案有两种,第一种是让子进程成为孤儿进程。第二种是父进程在子进程结束时收回资源。

        僵尸进程的状态是Z,每一个没被托管的子进程退出时都会有成为僵尸进程的状态,如果父进程马上处理这个状态就很短暂,反之就很长。

        fork使用wait等待子进程资源方式如下:

#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h> //wait的头文件

int main()
{
    pid_t pid;
    pid = fork();
    if (pid > 0) {
        std::cout << "Parent process" << '\n';
        wait(nullptr);
        return 0;
    }

    std::cout << "Child process" << '\n';
    return 0;
}

        wait会阻塞父进程直到子进程结束。wait接收一个int型的指针,会返回一些子进程信息。但是我们一般回收时不在意这些内容,因此一般可以设为nullptr。如果父进程有不止一个子进程那么wait会等待所有子进程。

        wait会阻塞父进程,因此它相对效率不高,而waitpid()使用信号的方式解决了这一问题。

        waitpid原型和参数如下:

pid_t waitpid(pid_t pid, int *status, int options);

第一个参数有四种情况:
1.pid<-1 等待进程组识别码为pid绝对值的任何子进程.
2.pid=-1 等待任何子进程.
3.pid=0 等待进程组识别码与目前进程相同的任何子进程.
4.pid>0 等待进程识别码为pid的子进程.
第二个参数status用于返回子进程结束状态值. 如果不需要结束状态值status可以传入NULL或nullptr.
第三个参数options代表返回规则,有很多种。
WNOHANG是常用的,表示如果没有已经结束的子进程马上返回, 不等待。
为0时和wait差不多,需要等待子线程结束

        使用方式如下:

#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
    pid_t pid;
    pid = fork();
    if (pid > 0) {
        std::cout << "Parent process" << '\n';
        //子进程结束就处理,没结束直接退出,让子进程成为孤儿进程。
        //也可以和信号singal结合设计更合理的方案
        waitpid(pid, nullptr, WNOHANG);
        return 0;
    }

    std::cout << "Child process" << '\n';
    return 0;
}

3.3 双fork让子进程变为孤儿进程:

        让子进程编程孤儿进程很容易想到创建一个子进程就exit,但如果父进程创建完子进程还需要处理事情,那么这种方案就比较麻烦。双fork可以解决次问题。

        双fork伪代码如下:

pid_t pid;
pid = fork();
if (pid > 0) {
    //进行父进程继续干的事
    return 0;
}

pid_t grandsonPid;
grandsonPid = fork();
if (grandsonPid > 0) {
    //子进程退出,此时孙子进程为孤儿进程
    exit(0);
}

3.4 守护进程:

        守护进程也是孤儿进程的一种,其可以用jobs查看。守护进程在后台运行,不与任何终端关联。守护进程一般在系统启动时就在运行但是也可以在某些时候运行。守护进程由root用户或者其他特殊用户运行,处理一些系统级的任务。

        由此看出创建守护进程必须让它变为孤儿进程,此外还有一些其它限制。最简单的守护进程步骤如下:

1.父进程fork并exit退出。
2.子进程调用setsid函数创建新的会话。
3.子进程调用chdir函数,让根目录 ”/” 成为子进程的工作目录。
4.子进程调用umask函数,设置进程的umask为0。

        实际上守护进程还需要很多考虑,比如使用双fork防止进程重新打开控制终端等处理方式。

4.子进程复制的资源:

4.1 拷贝资源的方式:

        子进程是从fork那一刻开始运行的,不过当在fork前面使用cout验证这一点时需要输出'\n'或std::endl,否则cout的内容一直留在缓冲区,而缓冲区也会被复制到子进程,因此子进程也会输出。

        子进程会拷贝父进程所有资源,对于动态内存方面采用深拷贝,会复制动态内存。但是对于文件描述符仅仅是浅拷贝,不会拷贝文件只会拷贝描述符。

        当查看子进程地址时会发现很多变量和动态内存地址和父进程一样,因此怀疑做的是浅拷贝。但实际上这些是虚拟地址,虚拟地址只有与物理地址映射才能存储数据。父进程和子进程的地址就是指向不同物理地址的虚拟地址。证明方式也很简单,子进程的变量值修改后父进程的不变。这是用了写时复制的结果。

4.2 写时复制:

        写时复制简单的说就是父子进程虚拟地址先映射到同一个物理地址,然后将该地址设为只读。当子进程或父进程往里写东西时复制一个物理空间和数据,并将子进程的虚拟地址映射到复制的物理空间。

5.进程间通信的方式介绍:

        进程的资源是互相隔离的,但是有些方式可以让它们通信。有八种常见的方式:

        1.匿名管道通信:管道是一种半双工的通信方式,数据只能单向流动。匿名管道只能在具有亲缘关系的进程间使用。
        2.高级管道通信:在该进程启动另一个程序,让另一个程序作为子进程。
        3.有名管道通信:和匿名管道相比有名管道可以在无亲缘关系进程间通信
        4.消息队列通信:   消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
        5.信号量通信:  信号量是一个计数器,可以用来控制多个进程对共享资源的访问。生产者消费者模型很多就是使用信号量。
        6.信号通信:即singal。信号和信号量是两个不一样的东西。
        7.共享内存通信:  共享内存就是多个进程的虚拟地址映射一个物理地址。
        8.套接字通信:使用socket在进程中传递消息。