Linux多进程开发

linux系统下每个进程都拥有自己的页表,父进程fork出新的子进程时,子进程拷贝一份父进程的页表,且父子进程将页表状态修改为写保护。当父进程或子进程发生写操作时将会发生缺页异常,缺页异常处理函数将会为子进程分配新的物理地址。

Linux 的 fork() 使用是通过写时拷贝实现。写时拷贝是一种可以推迟甚至避免拷贝数据的技术。内核并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。只有在写入时才会复制地址空间(重新开辟一块内存),从而使各个进程拥有自己的地址空间。即资源的复制只有在写入时才会进行,在此之前,只有以只读的方式进行。 fork() 之后的父子进程共享文件,此时的 fork() 产生的子进程与父进程相同的文件描述符指向相同的文件表,引用计数增加,共享文件偏移指针。 

/*
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
作用:创建子进程
    返回值:
        fork()返回值会返回两次,一次是在父进程中,一次是在子进程中
        在父进程中返回创建的子进程的ID,
        在子进程中,返回0
        如何区分父进程和子进程,通过fork的返回值
        在父进程中返回-1,表示创建子进程失败,并且设置errno


    父子进程之间的关系:
    区别:
        1.fork()函数的返回值不同
            父进程中:>0,返回子进程的PID
            子进程中:=0
        2.pcb中的一些数据
            当前进程的id pid
            当前进程的父进程的id ppid
            信号集
        共同点:
            某些状态下,子进程刚被创建出来,还没有执行任何的写数据操作
                -用户区的数据
                -文件描述符表
            父子进程对变量是否是共享的
                -刚开始是一样的,如果修改类数据,就不共享了
                读时共享(子进程刚创建),写时拷贝
                父子进程之间不能用变量进行通信,它们之间互不影响

*/
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main()
{

    int num =10;
    //创建子进程
    pid_t pid = fork();//pid_t本质上是int类型的
    //判断是父进程还是子进程
    if(pid>0){
        printf("pid : %d\n", pid);
        //如果大于0,返回的是创建的子进程的id,当前是父进程
        printf("i am parent process,pid : %d, ppid:%d\n",getpid(),getppid());
        printf("parent num: %d\n",num);
        num+=10;
        printf("parent num+=10: %d\n",num);
    }else if(pid ==  0)
    {
        //当前是子进程
        printf("i am child process, pid :%d,ppid : %d\n",getpid(),getppid());
        printf("child num: %d\n",num);
        num+=100;
        printf("child num+=100: %d\n",num);
    }
    for(int i =0;i<3;i++)//默认父子进程都会去执行
    {
        printf("i : %d\n", i);
        sleep(1);

    }
    return 0;
}

exec函数族

可执行文件的用户区去替换进程中的用户区,一般情况下会创建一个子进程,在子进程创建exec函数族中的函数

 

 

 

 

查看execl

 

 

 子进程被替换掉

 

 

 execlp

/*#include <unistd.h>

       extern char **environ;

       int execlp(const char *file, const char *arg, ...);
        -会到环境变量中查找指定的可执行文件,如果找到了就执行,找不到就
            参数:-file:需要指定的执行的可执行文件的文件名
                a.out    ps
                  -arg:是执行可执行文件所需要的参数列表
                    第一个参数一般没有什么作用,为了方便一般写的执行的参数的名称,从第二个人参数开始往后,就是程序执行所需要的参数列表
                    参数最后需要以NULL结束(哨兵)
             返回值:-仅仅在出错的时候返回,返回-1,并且设置errno
             如果调用成功,就没有返回值
                    
*/
#include <unistd.h>
#include <stdio.h>

int main()
{
    //创建一个子进程在子进程中执行exec函数族中的函数

    pid_t pid =fork();
    if(pid >0){
        //父进程
        printf("i am parent process,pid : %d\n",getpid());
        sleep(1);
    }else if(pid == 0)
    {
        //子进程
        execlp("ps","ps","aux",NULL);
        printf("i am child process , pid:%d\n",getpid());
    }
    for(int i =0;i<3;i++){
        printf("i=%d , pid = %d\n",i,getpid());
    }

    return 0;
}

第一个参数是可执行程序的程序名,第二个参数

 

 

 进程控制

fork函数  读时共享,写时复制      主要目的是降低内存的使用

 

 

子进程退出,父进程能得到子进程退出的状态       父进程有义务回收子进程的资源

 

#include <stdlib.h>
#include<unistd.h>
#include <stdio.h>
int main()
{
    printf("hello\n");
    printf("world");

    exit(0);

    return 0;  //和exit是一样的,都表示进程退出

}

结果  exit()       从图中两者的功能对比可以知 

 将exit()改成_exit()后      world在缓冲区里

标准c库的exit多做了一些事情(刷新缓冲区)

 

 孤儿进程

父进程运行结束,子进程还在运行,这样的子进程称为孤儿进程

◼ 每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为 init ,而 init进程会循环地 wait() 它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init 进程就会代表出面处理它的一切善后工作。

孤儿进程并不会有什么危害

如下,1领养了孤儿进程 ,由1进程回收资源

 

 父进程结束后,显示了一个终端(回到前台)

为什么都显示在终端?父子进程的内核去的一些部分是共享的   文件描述符表前三个(0,1,2)标准输入,输出,错误 

僵尸进程

◼ 每个进程结束之后, 都会释放自己地址空间中的用户区数据,内核区的 PCB 没有办法自己释放掉,需要父进程去释放。
◼ 进程终止时,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸
(Zombie)进程。

僵尸进程不能被kii -9杀死

僵尸进程            内核区数据没办法释放

 如何解决呢?

ctrl+c  发送一个9号信号把进程杀死    杀死父进程

 

 此时僵尸进程就不存在了      不过此方法只是在演示阶段使用,一般是wait 或waitpid

 wait

#include <sys/types.h>
#include <sys/wait.h>
#include<stdio.h>
#include<unistd.h>
#include <stdlib.h>
int main()
{
    //有一个父进程,创建5个子进程(兄弟)
    pid_t pid;
    for(int i=0;i<5;i++)    //这里不只产生5个
    {
        pid =fork();
        if(pid ==0)
        {
            break;       //此时子进程就不会产生孙子进程
        }
    }
    if(pid >0)
    {//父进程
        while(1)
            {
            printf("i am parent,pid = %d\n",getpid());
            //int ret = wait(NULL);
            int st;
            int ret = wait(&st);
            if(ret ==-1)
            {
                break;
            }

            if(WIFEXITED(st))
            {
                //是不是正常退出
                printf("退出的状态码:%d\n",WEXITSTATUS(st));
            }
            if(WIFSIGNALED(st))
            {
                //是不是异常终止
                printf("被哪个信号干掉了:%d\n",WTERMSIG(st));
            }
            printf("child die,pid =%d\n",ret);  //如果成功了,返回值就是子进程的id,失败返回-1
            sleep(1);
            }
    }
    else if (pid ==0){
        //子进程
        while(1)
        {
            printf("child,pid = %d\n",getpid());
            sleep(1);

        }
        exit(1);




    }
    return 0;

}

 

 

 通过信号杀死

kill -9

 默认是阻塞状态

非阻塞状态

int main()
{
    //有一个父进程,创建5个子进程(兄弟)
    pid_t pid;
    for(int i=0;i<5;i++)    //这里不只产生5个
    {
        pid =fork();
        if(pid ==0)
        {
            break;       //此时子进程就不会产生孙子进程
        }
    }
    if(pid >0)
    {//父进程
        while(1)
            {
            printf("i am parent,pid = %d\n",getpid());
            sleep(1);
            //int ret = wait(NULL);
            int st;
            int ret = waitpid(-1,&st,WNOHANG);//非阻塞
            if(ret ==-1)
            {
                break;
            }
            else if(ret == 0)
            {
                //还有子进程存在
                continue;
            }else if(ret>0)//回收到某一个具体的子进程
            {

                if(WIFEXITED(st))
                {
                    //是不是正常退出
                    printf("退出的状态码:%d\n",WEXITSTATUS(st));
                }
                if(WIFSIGNALED(st))
                {
                    //是不是异常终止
                    printf("被哪个信号干掉了:%d\n",WTERMSIG(st));
                }
                printf("child die,pid =%d\n",ret);  //如果成功了,返回值就是子进程的id,失败返回-1
            }
            sleep(1);
            }
    }
    else if (pid ==0){
        //子进程
        while(1)
        {
            printf("child,pid = %d\n",getpid());
            sleep(1);
        }
        exit(1);
    }
    return 0;
}

父进程可继续往下执行

 进程间通信

进程是一个独立的资源分配单元,不同进程(这里所说的进程通常指的是用户进程)之间的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源。

进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信(IPC)

 03匿名管道

◼ 管道也叫无名(匿名)管道,它是是 UNIX 系统 IPC(进程间通信)的最古老形式,所有的 UNIX 系统都支持这种通信机制。

统计一个目录中文件的数目命令:ls | wc –l,为了执行该命令,shell 创建了两个进程来分别执行 ls 和 wc。

 

 

管道两端对应两个文件描述符支持读操作和写操作

一个管道是一个字节流,使用管道时不存在消息或者消息边界的概念,从管道读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是多少

 通过管道传递的数据是顺序的,从管道中读取出来的字节的顺序和它们被写入管道的顺序是完全一样的。

在管道中的数据的传递方向是单向的,一端用于写入,一端用于读取,管道是半双工的。   全双工:

半双工:就像对讲机    需要CSMA/CD

◼ 从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据,在管道中无法使用 lseek() 来随机的访问数据。

匿名管道只能在具有公共祖先的进程(父进程与子进程,或者两个兄弟进程,具有亲缘关系)之间使用。

 

 

 管道数据结构

循环队列

 

  示例:

/*
 #include <unistd.h>

       int pipe(int pipefd[2]);

    功能:创建一个匿名管道,用来进程间通信
    参数:int pipefd[2]这个数组是一个传出参数
        Pipefd[0]对应的是管道的读端
        pipefd[1]对应的是管道的写端
    返回值: 成功返回0,失败返回-1
    注意:匿名管道只能用于具有关系的进程间的通信(父子,兄弟)
*/
//子进程发送数据给父进程,父进程读取到数据输出
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include<string.h>
int main()
{
    //在fork()之前创建管道
    int pipefd[2];

    int ret = pipe(pipefd);
    if(ret == -1)
    {
        perror("pipe");
        exit(0);
    }
    //创建子进程
    pid_t pid = fork();
    if(pid>0)
    {
        //父进程
        //从管道的读取段读取数据
        char buf[1024]={0};
        int len = read(pipefd[0],buf,sizeof(buf));
        printf("parent recv : %s,pid : %d\n",buf,getpid());
    }
    else if(pid == 0)
    {
        sleep(10);
        //子进程
        char* str = "hello,i am child";
        write(pipefd[1],str,strlen(str));
    }


    return 0;
}

只有当管道里面有数据,才能去读,不然处于

 父子进程相互读写

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include<string.h>
int main()
{
    //在fork()之前创建管道
    int pipefd[2];

    int ret = pipe(pipefd);
    if(ret == -1)
    {
        perror("pipe");
        exit(0);
    }
    //创建子进程
    pid_t pid = fork();
    if(pid>0)
    {
        printf("i am parent process pid : %d\n",getpid());
        //父进程
        //从管道的读取段读取数据
        char buf[1024]={0};
        while(1)
        {
        int len = read(pipefd[0],buf,sizeof(buf));
        printf("parent recv : %s,pid : %d\n",buf,getpid());


        char* str = "hello,i am parent";
        write(pipefd[1],str,strlen(str));
        sleep(1);
        }

    }
    else if(pid == 0)
    {
        sleep(10);
        //子进程
    printf("i am child process,pid : %d\n",getpid());
     char buf[1024]={0};
        while(1)
        {
        //向管道中写入数据
        char* str = "hello,i am child";
        write(pipefd[1],str,strlen(str));
        sleep(1);

        int len = read(pipefd[0],buf,sizeof(buf));
        printf("child recv : %s,pid : %d\n",buf,getpid());
        }

    }


    return 0;
}

ulimit -a    管道大小:8块  512字节/块    4k

 

//查看管道的大小
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include<string.h>
int main()
{
    int pipefd[2];

    int ret = pipe(pipefd);
    //获取管道的大小
    long size = fpathconf(pipefd[0],_PC_PATH_MAX);
    printf("pipe size : %ld\n",size);

    return 0;
}

 存在t的原因,是没有清除数组

 

为什么会出现这种情况?

 

 

 自己写入的数据被自己读了

怎么解决这个问题?   父子进程要么读,要么写,一般情况下不会出现一个子进程又读又写

 管道非阻塞

        char buf[1024]={0};
        int flags = fcntl(pipefd[0],F_GETFL);//获取原来的flag标记
        flags |= O_NONBLOCK;    //修改flag的值
        fcntl(pipefd[0],F_SETFL,flags);  //设置新的flag

有名管道

FIFO  提供了一个路径名与之关联  其打开方式与打开一个普通文件是一样的   只要可以访问该路径,就能够彼此通过FIFO通信

有名管道的使用

mkfifo 名字          严格遵循先进先出

方法1: 通过命令创建

 

 方法2:

通过函数创建

int main()
{

    int ret = mkfifo("fifo1",0664);
    if(ret == -1)
    {
        perror("mkfifo");
        exit(0);
    }
    return 0;
}

 

posted @ 2021-07-08 21:38  wsq1219  阅读(71)  评论(0编辑  收藏  举报