Linux高并发服务器之第二章---Linux多进程开发

目录

2.1进程

 2.3进程创建

2.4父子进程虚拟地址空间

2.4.1父子进程虚拟地址空间

2.5 GDB多进程调试

 2.6 exec函数族

 execl()函数

 execlp()函数

2.7进程控制

僵尸进程 

2.10进程间通信(IPC  inter process communication)

 2.12  父子进程通过匿名管道通信

2.14管道的读写特点以及管道设置为非阻塞

2.15有名管道 

 2.16使用有名管道完成聊天的功能

 2.17内存映射​编辑


2.1进程

2.1.1  进程的含义

我们编写的代码只是⼀个存储在硬盘的静态⽂件,通过编译后就会⽣成⼆进制可执⾏⽂件,当我们运⾏这 个可执⾏⽂件后,它会被装载到内存中,接着 CPU 会执⾏程序中的每⼀条指令,那么这个 运⾏中的程序, 就被称为「进程」( Process

 

 2.1.2单道和多道程序设计

 2.1.3 时间片

 2.1.4并行与并发

 2.1.5 进程控制块(PCB)

 2.2进程状态转换

2.2.1进程的状态

 

2.2.2 进程相关命令

 

 

 

 

 2.3进程创建

2.3.1进程创建

fork()创建父子进程,写时复制,读时共享

对于fork()的理解,画进程图会更有帮助

例如对于下面这段代码

int main()
{
   Fork();
   Fork();
   printf("hello\n");
   exit(0);

}

 fork()函数的特点:

1.调用一次,返回两次(一次返回父进程,一次返回子进程)

2.并发执行(父进程和子进程是并发运行的独立进程)

3.相同但是独立的地址空间

4.共享文件

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

       pid_t fork(void);
       函数的作用:用于创建子进程
       返回值:
            fork()的返回值会返回两次,一次在父进程中,一次在子进程中
            在父进程中返回创建的子进程的ID,
            在子进程中返回0
            如何区分父进程和子进程:通过fork的返回值,
            在父进程中返回-1,表示创建子进程失败,并且设置errno
*/

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

       #include<stdio.h>

int main()
{
    //创建子进程
     pid_t pid =fork();
     /*
     实际上,Linux()的fork()使用是通过写时拷贝,实际上,写时拷贝是一种可以推迟甚至避免拷贝数据的技术
     内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。
     只用在需要写入的时候才会复制地址空间,而是让父子进程共享同一个地址空间。
     也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享
     注意:fork之后父子进程共享文件
     fork产生的子进程与父进程相同的文件,文件描述符指向相同的文件表,引用计数增加

        父子进程之间的关系:
        区别:
        1.fork()函数的返回值不同
           父进程中:>0,返回的子进程ID
           子进程中:=0;
        2.pcb中的一些数据
           当前的进程的id,pid
           当前的进程的父进程的id ppid
          信号集 

       共同点:
        某些状态下,子进程 刚被创建出来,还没有执行任何写数据的操作
        -用户区的数据
        -文件描述符表

        父子进程对变量是不是共享的?
        -刚开始的时候,是一样的,共享的;如果修改了数据,就不共享了
        -读时共享(子进程被创建),写时拷贝。
     */

     int num=10;

     //判断,判断是父进程还是子进程
     if(pid>0)
     {
        printf("pid:%d\n",pid);
        //如果大于0,返回的是创建的子进程的进程号,当前是父进程
        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());
       //getpid()返回的是当前进程号,getppid返回的是他的父进程的进程号
        printf("parent num:%d\n",num);
        num+=100;
        printf("parent num+=10:%d\n",num);
     }

     //for循环
     for(int i=0;i<3;i++)
     {
        printf("i:%d,pid:%d\n\n",i,getpid());
        //sleep(1);
     }
    return 0;
}

 2.3.2父子进程虚拟地址空间

 

 当进程是父进程时,pid=子进程id;当进程是子进程时,pid等于0;

2.4父子进程虚拟地址空间

     实际上,Linux()的fork()使用是通过写时拷贝,实际上,写时拷贝是一种可以推迟甚至避免拷贝数据的技术

     内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。

     只用在需要写入的时候才会复制地址空间,而是让父子进程共享同一个地址空间。

     也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享

     注意:fork之后父子进程共享文件

     fork产生的子进程与父进程相同的文件,文件描述符指向相同的文件表,引用计数增加

2.4.1父子进程虚拟地址空间

在操作系统中,每个进程都有自己的虚拟地址空间,这个空间包括了进程所能访问的内存区域。在UNIX-like操作系统中,当一个进程创建子进程时,子进程会复制父进程的虚拟地址空间,包括了父进程的代码、数据和堆栈等区域。

父子进程之间的虚拟地址空间是独立的,即它们各自拥有自己的虚拟地址空间,并且这些空间是相互独立的。这意味着,如果一个进程修改了自己的虚拟地址空间中的数据,这个修改不会影响到其他进程的虚拟地址空间中相同的数据。

在创建子进程时,操作系统会在子进程的虚拟地址空间中创建一个新的进程映像,并将父进程的进程映像复制到子进程的虚拟地址空间中。这个复制包括了父进程的代码、数据和堆栈等区域,但是父进程和子进程的进程映像是独立的,它们各自拥有自己的虚拟地址空间,修改一个进程的虚拟地址空间不会影响到另一个进程的虚拟地址空间。

因此,父子进程之间可以共享数据,但是它们各自维护自己的虚拟地址空间,这样就能保证进程之间的独立性和隔离性。

2.5 GDB多进程调试

 给父子进程打断点

 运行起来,可以发现GDB调试的是调试父进程

 设置跟踪子进程

 2.6 exec函数族

2.6.1 exec函数族介绍

2.6.2 execl()函数

在Linux系统中,exec()函数族用于执行一个新的程序,替换当前进程的内容。

/*  
    #include <unistd.h>
    int execl(const char *path, const char *arg, ...);
        - 参数:
            - path:需要指定的执行的文件的路径或者名称
                a.out /home/nowcoder/a.out 推荐使用绝对路径
                ./a.out hello world

            - arg:是执行可执行文件所需要的参数列表
                第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称
                从第二个参数开始往后,就是程序执行所需要的的参数列表。
                参数最后需要以NULL结束(哨兵)

        - 返回值:
            只有当调用失败,才会有返回值,返回-1,并且设置errno
            如果调用成功,没有返回值。

*/
       #include <sys/types.h>
       #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) {
        // 子进程
        // execl("hello","hello",NULL);

        execl("/bin/ps", "ps", "aux", NULL);
        perror("execl");
        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;
}

2.6.3 execlp()函数

与execl()函数的区别就是在于execlp()函数会到环境变量中查找指定的可执行文件,如果找到了就执行,找不到就执行不成功。

/*  
    #include <unistd.h>
    int execlp(const char *file, const char *arg, ... );
        - 会到环境变量中查找指定的可执行文件,如果找到了就执行,找不到就执行不成功。
        - 参数:
            - file:需要执行的可执行文件的文件名
                a.out
                ps

            - arg:是执行可执行文件所需要的参数列表
                第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称
                从第二个参数开始往后,就是程序执行所需要的的参数列表。
                参数最后需要以NULL结束(哨兵)

        - 返回值:
            只有当调用失败,才会有返回值,返回-1,并且设置errno
            如果调用成功,没有返回值。


        int execv(const char *path, char *const argv[]);
        argv是需要的参数的一个字符串数组
        char * argv[] = {"ps", "aux", NULL};
        execv("/bin/ps", argv);

        int execve(const char *filename, char *const argv[], char *const envp[]);
        char * envp[] = {"/home/nowcoder", "/home/bbb", "/home/aaa"};


*/
       #include <sys/types.h>
       #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;
}

2.7进程控制

2.7.1进程退出

/*
    #include <stdlib.h>
    void exit(int status);

    #include <unistd.h>
    void _exit(int status);

    status参数:是进程退出时的一个状态信息。父进程回收子进程资源的时候可以获取到。
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {

    printf("hello\n");
    printf("world");

     //exit(0);
    _exit(0);
    
    return 0;
}

hello\n的换行符需要刷新IO口

结果

2.7.2孤儿进程

orphan.c

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

       pid_t fork(void);
       函数的作用:用于创建子进程
       返回值:
            fork()的返回值会返回两次,一次在父进程中,一次在子进程中
            在父进程中返回创建的子进程的ID,
            在子进程中返回0
            如何区分父进程和子进程:通过fork的返回值,
            在父进程中返回-1,表示创建子进程失败,并且设置errno
*/

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

       #include<stdio.h>

int main()
{
    //创建子进程
     pid_t pid =fork();
     /*
     实际上,Linux()的fork()使用是通过写时拷贝,实际上,写时拷贝是一种可以推迟甚至避免拷贝数据的技术
     内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。
     只用在需要写入的时候才会复制地址空间,而是让父子进程共享同一个地址空间。
     也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享
     注意:fork之后父子进程共享文件
     fork产生的子进程与父进程相同的文件,文件描述符指向相同的文件表,引用计数增加

        父子进程之间的关系:
        区别:
        1.fork()函数的返回值不同
           父进程中:>0,返回的子进程ID
           子进程中:=0;
        2.pcb中的一些数据
           当前的进程的id,pid
           当前的进程的父进程的id ppid
          信号集 

       共同点:
        某些状态下,子进程 刚被创建出来,还没有执行任何写数据的操作
        -用户区的数据
        -文件描述符表

        父子进程对变量是不是共享的?
        -刚开始的时候,是一样的,共享的;如果修改了数据,就不共享了
        -读时共享(子进程被创建),写时拷贝。
     */

     int num=10;

     //判断,判断是父进程还是子进程
     if(pid>0)
     {
        printf("pid:%d\n",pid);
        //如果大于0,返回的是创建的子进程的进程号,当前是父进程
        printf("i am parent process,pid:%d,ppid:%d\n",getpid(),getppid());



     }else if(pid==0)
     {
        sleep(1);
        //当前是子进程
        printf("i am child process,pid:%d,ppid:%d\n",getpid(),getppid());
       
     }

     //for循环
     for(int i=0;i<3;i++)
     {
        printf("i:%d,pid:%d\n\n",i,getpid());
        //sleep(1);
     }
    return 0;
}

2.7.3 僵尸进程 

ps -aux查看进程

zombie.c

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

       pid_t fork(void);
       函数的作用:用于创建子进程
       返回值:
            fork()的返回值会返回两次,一次在父进程中,一次在子进程中
            在父进程中返回创建的子进程的ID,
            在子进程中返回0
            如何区分父进程和子进程:通过fork的返回值,
            在父进程中返回-1,表示创建子进程失败,并且设置errno
*/

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

       #include<stdio.h>

int main()
{
    //创建子进程
     pid_t pid =fork();
     /*
     实际上,Linux()的fork()使用是通过写时拷贝,实际上,写时拷贝是一种可以推迟甚至避免拷贝数据的技术
     内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。
     只用在需要写入的时候才会复制地址空间,而是让父子进程共享同一个地址空间。
     也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享
     注意:fork之后父子进程共享文件
     fork产生的子进程与父进程相同的文件,文件描述符指向相同的文件表,引用计数增加

        父子进程之间的关系:
        区别:
        1.fork()函数的返回值不同
           父进程中:>0,返回的子进程ID
           子进程中:=0;
        2.pcb中的一些数据
           当前的进程的id,pid
           当前的进程的父进程的id ppid
          信号集 

       共同点:
        某些状态下,子进程 刚被创建出来,还没有执行任何写数据的操作
        -用户区的数据
        -文件描述符表

        父子进程对变量是不是共享的?
        -刚开始的时候,是一样的,共享的;如果修改了数据,就不共享了
        -读时共享(子进程被创建),写时拷贝。
     */

     int num=10;

     //判断,判断是父进程还是子进程
     if(pid>0)
     {
        while(1)
        {
        //如果大于0,返回的是创建的子进程的进程号,当前是父进程
        printf("i am parent process,pid:%d,ppid:%d\n",getpid(),getppid());
        sleep(1);
        }


     }else if(pid==0)
     {
        //僵尸进程就是子进程死了,但是父进程没有回收子进程ID
        //当前是子进程
        printf("i am child process,pid:%d,ppid:%d\n",getpid(),getppid());
       
     }

     //for循环
     for(int i=0;i<3;i++)
     {
        printf("i:%d,pid:%d\n\n",i,getpid());
        //sleep(1);
     }
    return 0;
}

2.7.4 进程回收

 status将包含子进程的退出状态,该状态可以通过WEXITSTATUS(status)宏来获取。

在Linux系统中,WEXITSTATUS(status)是一个用于解析子进程退出状态的宏定义。status是一个整型变量,它包含了子进程的退出状态以及一些其他信息。

WEXITSTATUS(status)会解析子进程的退出状态,返回子进程的退出状态码。如果子进程是通过调用exit或者_exit正常退出的,那么status将包含子进程的退出状态,该状态码可以通过WEXITSTATUS(status)宏来获取。

 2.7.5  wait与waitpid

wait.c

在Linux系统中,wait()是一个用于等待子进程结束并获取其退出状态的系统调用。当一个进程调用fork()创建子进程后,它可以使用wait()系统调用来等待子进程的结束并获取其退出状态。

wait()系统调用会阻塞当前进程,直到它的任何一个子进程结束为止。一旦一个子进程结束,wait()系统调用将返回该子进程的PID和退出状态。

wait()系统调用可以防止父进程在子进程还没有完成时退出。这对于保持进程层次结构的完整性非常重要,因为如果父进程在子进程还没有完成时退出,那么子进程将成为孤儿进程并被系统的init进程接管。

/*
    #include <sys/types.h>
    #include <sys/wait.h>
    pid_t wait(int *wstatus);
        功能:等待任意一个子进程结束,如果任意一个子进程结束了,次函数会回收子进程的资源。
        参数:int *wstatus
            进程退出时的状态信息,传入的是一个int类型的地址,传出参数。
        返回值:
            - 成功:返回被回收的子进程的id
            - 失败:-1 (所有的子进程都结束,调用函数失败)

    调用wait函数的进程会被挂起(阻塞),直到它的一个子进程退出
或者收到一个不能被忽略的信号时才被唤醒(相当于继续往下执行)
    如果没有子进程了,函数立刻返回,返回-1;
如果子进程都已经结束了,也会立即返回,返回-1.

*/
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>


int main() {

    // 有一个父进程,创建5个子进程(兄弟)
    pid_t pid;

    // 创建5个子进程
    for(int i = 0; i < 5; i++) {
        pid = fork();
        if(pid == 0) {
            break;
        }
    }

    if(pid > 0) {
        // 父进程
        while(1) {
            printf("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);

            sleep(1);
        }

    } else if (pid == 0){
        // 子进程
         while(1) {
            printf("child, pid = %d\n",getpid());    
            sleep(1);       
         }

        exit(0);
    }

    return 0; // exit(0)
}

waitpid.c

       pid_t waitpid(pid_t pid, int *wstatus, int options);
          功能:回收指定进程号的子进程,可以设置是否阻塞
          参数:
          pid:
              pid>0:某个子进程的pid
              pid=0:回收当前进程组的所有子进程           ps ajx能够查看进程组ID
              pid=-1:回收所有子进程,相当于wait() (最常用)
              pid <-1:回收某个进程组的组id

        -wstatus:存放关于导致返回的子进程的状态的状态信息
        -options:设置阻塞或者非阻塞
              0:阻塞
              WNOHANG:非阻塞
        -返回值:
              >0:返回子进程的id
              =0:options=WNOHANG,表示还有子进程
              =-1:错误,或者没有子进程了
*/

/*
       #include <sys/types.h>
       #include <sys/wait.h>
       pid_t waitpid(pid_t pid, int *wstatus, int options);
          功能:回收指定进程号的子进程,可以设置是否阻塞
          参数:
          pid:
              pid>0:某个子进程的pid
              pid=0:回收当前进程组的所有子进程           ps ajx能够查看进程组ID
              pid=-1:回收所有子进程,相当于wait() (最常用)
              pid <-1:回收某个进程组的组id
        -options:设置阻塞或者非阻塞
              0:阻塞
              WNOHANG:非阻塞
        -返回值:
              >0:返回子进程的id
              =0:options=WNOHANG,表示还有子进程
              =-1:错误,或者没有子进程了
*/

#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>


int main() {

    // 有一个父进程,创建5个子进程(兄弟)
    pid_t pid;

    // 创建5个子进程
    for(int i = 0; i < 5; i++) {
        pid = fork();
        if(pid == 0) {
            break;
        }
    }

    if(pid > 0) {
        // 父进程
        while(1) {
            printf("parent, pid = %d\n", getpid());
            sleep(1);

            // int ret = wait(NULL);
            int st;//子进程状态
            int ret = waitpid(-1,&st,WNOHANG);
            /*
              >0:返回子进程的id
              =0:options=WNOHANG,表示还有子进程
              =-1:错误,或者没有子进程了
            */

            if(ret == -1) {
                //说明没有子进程了
                break;
            }

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

            }

          
            
        }

    } else if (pid == 0){
        // 子进程
         while(1) {
            printf("child, pid = %d\n",getpid());    
            sleep(1);       
         }

        exit(0);
    }

    return 0; // exit(0)
}

2.8 进程间通信(IPC  inter process communication)

GUI:Graphical User Interface  图像用户界面

IDE:Integrated Development Environment  集成开发环境

API:Application Programming Interface  应用程序接口

2.8.1 进程间通讯概念

 2.8.2 Linux进程间通信的方式

 2.8.3匿名管道

 2.8.4管道的特点

 

 2.8.5  使用管道进行进程间通信

 2.8.6管道的数据结构

 2.12  父子进程通过匿名管道通信

 实现父子进程间的匿名通信

pipe.c

/*
  #include<unistd.h>
  int pipe(int pipefd[2]);
         功能:创建一个匿名管道,用来进程间通信
         参数:int pipefd[2],这个数组是一个传出参数
               pipefd[0] 对应的是管道读端
               pipefd[1] 对应的是管道的写端
         返回值:
             成功 0
             失败 -1

  注意:匿名管道只能用于具有关系的进程之间的通信(父子进程,兄弟进程)

  管道默认是阻塞的:如果管道中没有数据,read阻塞,如果管道满了,write阻塞
*/

 //
 #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)
         {
         read(pipefd[0],buf,sizeof(buf));// pipefd[0] 对应的是管道读端,如果管道没有数据,就会阻塞在这里
         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);
         //子进程
         char buf1[1024]={0};
         printf("i am child process,pid:%d\n",getpid());
         while(1)
         {
            //发送数据
         char*str ="hello,i am child";
         write(pipefd[1],"hello,i am child",strlen(str));//pipefd[1] 对应的是管道的写端
         sleep(1);

         //向管道读取数据
         int len =read(pipefd[0],buf1,sizeof(buf1));// pipefd[0] 对应的是管道读端,如果管道没有数据,就会阻塞在这里
         printf("child recv :%s,pid:%d\n",buf1,getpid());
         }

         //
      }

      return 0;
 }

结果:

实现了通信 

查看管道缓冲大小

fpathconf.c

 /*
 
  //查看管道缓冲大小函数
       #include <unistd.h>

       long fpathconf(int fd, int name);
       long pathconf(const char *path, int name);
       */
 
 
 #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_PIPE_BUF);
     
    printf("pipe size:%ld\n",size);
    

    return 0;
 }

结果:

管道默认大小:4k

  

        父子进程间通信实现ps aux|grep xx功能

 
 /*
    实现ps aux|grep xx功能
    父子进程间通信

    子进程:ps aux,子进程结束后,将数据发送给父进程
    父进程:获取到数据,过滤

    pipe()
    execlp()
    子进程将标准输出重定向到管道的写端
 */
 
 #include <unistd.h>
 #include <sys/types.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <wait.h>

 int main()
 {
    //创建一个管道
    int fd[2];
    int ret =pipe(fd);

    if(ret ==-1)
    {
        perror("pipe");
        exit(0);
    }

    //创建子进程
    pid_t pid =fork();

    if(pid>0)
    {
        //父进程
        //关闭写端
        close(fd[1]);
        //从管道中读取
        char buf[1024];

        int len=read(fd[0],buf,sizeof(buf)-1);
        while(len>0)
        {
        //过滤数据输出
        printf("%s",buf);
        memset(buf,0,1024);
        }

        wait(NULL);

    }


    else if(pid ==0)
    {
        //子进程
        //文件描述符的重定向  stdout_fileno ->fd[1]
        dup2(fd[1],STDOUT_FILENO);
        //执行ps aux
        execlp("ps","ps","aux",NULL);
        perror("execlp");
        exit(0);
        
    }
    return 0;
 }

2.14管道的读写特点以及管道设置为非阻塞

管道的读写特点:
使用管道时,需要注意以下几种特殊的情况(假设都是阻塞I/O操作)
  1.所有的指向管道写端的文件描述符都关闭了(管道写端引用计数为0),有进程从管道的读端读数据,那么管道中剩余的数据被读取以后,再次read会返回0,就像读到文件末尾一样。

  2.如果有指向管道写端的文件描述符没有关闭(管道的写端引用计数大于0),而持有管道写端的进程
也没有往管道中写数据,这个时候有进程从管道中读取数据,那么管道中剩余的数据被读取后,再次read会阻塞,直到管道中有数据可以读了才读取数据并返回。

  3.如果所有指向管道读端的文件描述符都关闭了(管道的读端引用计数为0),这个时候有进程向管道中写数据,那么该进程会收到一个信号SIGPIPE, 通常会导致进程异常终止。

  4.如果有指向管道读端的文件描述符没有关闭(管道的读端引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道中写数据,那么在管道被写满的时候再次write会阻塞,直到管道中有空位置才能再次写入数据并返回。

总结:
    读管道:
        管道中有数据,read返回实际读到的字节数。
        管道中无数据:
            写端被全部关闭,read返回0(相当于读到文件的末尾)
            写端没有完全关闭,read阻塞等待

    写管道:
        管道读端全部被关闭,进程异常终止(进程收到SIGPIPE信号)
        管道读端没有全部关闭:
            管道已满,write阻塞
            管道没有满,write将数据写入,并返回实际写入的字节数

设置管道为非阻塞

noblock.c 

/*
  #include<unistd.h>
  int pipe(int pipefd[2]);
         功能:创建一个匿名管道,用来进程间通信
         参数:int pipefd[2],这个数组是一个传出参数
               pipefd[0] 对应的是管道读端
               pipefd[1] 对应的是管道的写端
         返回值:
             成功 0
             失败 -1

  注意:匿名管道只能用于具有关系的进程之间的通信(父子进程,兄弟进程)

  管道默认是阻塞的:如果管道中没有数据,read阻塞,如果管道满了,write阻塞



   
*/

 //
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>

 /*
    设置管道非阻塞
    int flags=fcntl(fd[0],F_GETFRL);//获取原来的flag
    flags|=O_nonblock;  //修改flags的值
    fcntl(fd[0],F_SETFL,flags);
 */

 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());

        //关闭写端
        close(pipefd[1]);

       //从管道的读取端读取数据
         char buf[1024]={0};
         //设置管道为非阻塞
        int flags = fcntl(pipefd[0], F_GETFL);  // 获取原来的flag
        flags |= O_NONBLOCK;            // 修改flag的值
        fcntl(pipefd[0], F_SETFL, flags);   // 设置新的flag
         while(1)
         {
         int len=read(pipefd[0],buf,sizeof(buf));// pipefd[0] 对应的是管道读端,如果管道没有数据,就会阻塞在这里
         printf("len:% d\n",len);
         printf("parent recv :%s,pid:%d\n",buf,getpid());
         sleep(1);
         }
  

      }
      else if(pid ==0)
      {
          //sleep(10);
         //子进程
         char buf1[1024]={0};
         printf("i am child process,pid:%d\n",getpid());

         //关闭读端
         close(pipefd[0]);
         while(1)
         {
            //发送数据
         char*str ="hello,i am child\n";
         write(pipefd[1],"hello,i am child",strlen(str));//pipefd[1] 对应的是管道的写端
         sleep(10);

         }

         //
      }

      return 0;
 }

2.15有名管道 

 

 

有名管道的读与写 

 write.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

// 向管道中写数据
/*
    有名管道的注意事项:
        1.一个为只读而打开一个管道的进程会阻塞,直到另外一个进程为只写打开管道
        2.一个为只写而打开一个管道的进程会阻塞,直到另外一个进程为只读打开管道

    读管道:
        管道中有数据,read返回实际读到的字节数
        管道中无数据:
            管道写端被全部关闭,read返回0,(相当于读到文件末尾)
            写端没有全部被关闭,read阻塞等待
    
    写管道:
        管道读端被全部关闭,进行异常终止(收到一个SIGPIPE信号)
        管道读端没有全部关闭:
            管道已经满了,write会阻塞
            管道没有满,write将数据写入,并返回实际写入的字节数。
*/
int main() {

    // 1.判断文件是否存在
    int ret = access("test", F_OK);
    if(ret == -1) {
        printf("管道不存在,创建管道\n");
        
        // 2.创建管道文件
        ret = mkfifo("test", 0664);

        if(ret == -1) {
            perror("mkfifo");
            exit(0);
        }       

    }

    // 3.以只写的方式打开管道
    int fd = open("test", O_WRONLY);//没有读端,就会在此阻塞
    if(fd == -1) {
        perror("open");
        exit(0);
    }

    // 写数据
    for(int i = 0; i < 100; i++) {
        char buf[1024];
        sprintf(buf, "hello, %d\n", i);
        printf("write data : %s\n", buf);
        write(fd, buf, strlen(buf));
        sleep(1);
    }

    close(fd);

    return 0;
}

read.c 

//从管道写数据
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<string.h>
#include<stdlib.h>
#include <unistd.h>
#include<fcntl.h>
//从管道读取数据
int main()
{
    //1.打开管道文件
    int fd=open("test",O_RDONLY);
    if(fd==-1)
    {
        perror("open");
        exit(0);
    }
    
    //读数据
    while(1)
    {
        char buf[1024]={0};
        int len =read(fd,buf,sizeof(buf));
        if(len==0)//即写端没有写入数据
        {
            printf("写段断开连接...\n");
            break;
        }
        printf("recv buf :%s\n",buf);
        
    }

    return  0;
}

 2.16使用有名管道完成聊天的功能

 这种只创建一个进程去实现两端读写通信是存在弊端的:因为它只能实现一端的读或者写,不能两端的读和写一起进行,因为当进程A没有发送数据时,进程B会在read函数中阻塞;

解决这个问题的方法是:创建两个进程,分别去实现读与写操作

 chatA.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>

int main()
{
    //1.判断有名管道是否存在,不存在创建出来
     int ret = access("test", F_OK);
     if(ret==-1)
     {
        //文件不存在
        printf("文件不存在,创建管道文件\n");
        ret=mkfifo("fifo1",0664);
        
        if(ret==-1)
        {
            perror("mkfifo");
            exit(0);
        }
     }

     //2.以只写的方式打开fifo1
     int fdw=open("fifo1",O_WRONLY);
     if(fdw==-1)
     {
        perror("open");
        exit(0);
     }
     printf("打开fifo1成功,等待写入数据......\n");
     
     //以只读的方式打开管道fifo2
     int fdr =open("fifo2",O_RDONLY);
     if(fdr==-1)
     {
        perror("open");
        exit(0);
     }
    printf("打开fifo2成功,等待读取数据......\n");
     
     char buf[128];
     //4.循环地写读数据
     while(1)
     {
        memset(buf,0,128);//memset 函数是 C 语言标准库中的一个函数,通常被用于将一段内存空间的值设置为指定的值。
        //在这里的作用是把buf清空
        //获取标准输入的数据
        fgets(buf,128,stdin);
        //写数据
        ret =write(fdw,buf,strlen(buf));
        if(ret==-1)
        {
            perror("write");
            break;
        }
        //读数据
        memset(buf,0,128);
        ret =read(fdr,buf,sizeof(buf));//fdr文件描述符
        if(ret<=0)//ret=-1,说明读取失败,ret=0,说明读到管道末尾
        {
          perror("read");
          break;
        }
        printf("buf:%s\n",buf);
        
     }
     //关闭文件描述符
     close(fdr);
     close(fdw);

     return 0;
}

chatB.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

int main()
{
        //1.判断有名管道是否存在,不存在创建出来
     int ret = access("test", F_OK);
     if(ret==-1)
     {
        //文件不存在
        printf("文件不存在,创建管道文件\n");
        ret=mkfifo("fifo2",0664);
        
        if(ret==-1)
        {
            perror("mkfifo");
            exit(0);
        }
     }

      //2.以只读的方式打开管道fifo1
     int fdr =open("fifo1",O_RDONLY);
     if(fdr==-1)
     {
        perror("open");
        exit(0);
     }
    printf("打开fifo1成功,等待读取数据......\n");

      //3.以只写的方式打开fifo2
     int fdw=open("fifo2",O_WRONLY);
     if(fdw==-1)
     {
        perror("open");
        exit(0);
     }
     printf("打开fifo2成功,等待写入数据......\n");
     
     char buf[128];
     //4.循环地读写数据
     while(1)
     {

        //读数据
        memset(buf,0,128);
        ret =read(fdr,buf,sizeof(buf));//fdr文件描述符
        if(ret<=0)//ret=-1,说明读取失败,ret=0,说明读到管道末尾
        {
          perror("read");
          break;
        }
        printf("buf:%s\n",buf);

        //获取标准输入的数据
        memset(buf,0,128);//memset 函数是 C 语言标准库中的一个函数,通常被用于将一段内存空间的值设置为指定的值。
        //在这里的作用是把buf清空
      
        fgets(buf,128,stdin);
        //写数据
        ret =write(fdw,buf,strlen(buf));
        if(ret==-1)
        {
            perror("write");
            break;
        }

        
     }
     //关闭文件描述符
     close(fdr);
     close(fdw);


    return 0;
}

 2.17内存映射

 使用内存映射区完成父子进程间通信

mmap-parent-child-ipc.c
 

/*

       #include <sys/mman.h>

       void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
               -功能:将一个文件或者设备的数据映射到内存中
               -参数:
                   -void *addr:NULL,由内核指定
                   -length;要映射的数据的长度,这个值不能为0,建议使用文件的长度
                   获取文件的长度:stat lseek
                   -prot:对申请的内存映射区的操作权限
                      -PROT_EXEC:可执行的权限
                      -PROT_READ:读权限
                      -PROT_WRITE:写权限
                      -PROT_NONE:没有权限
                      要操作映射区,必须要有读的权限
                     一般使用 PROT_READ,PROT_READ|PROT_WRITE
                   -flags;
                     -MAP_SHARED:映射区的数据会自动和磁盘文件进行同步,进程间通信,必须要设置这个选项
                     -MAP_PRIVATE:不同步,内存映射区的数据改变了,对原来的文件不会修改,会重新创造一个新的文件。(copy on write)
                   -fd;需要映射的文件的文件描述符
                       -通过open得到,open的是一个磁盘文件
                       -注意:文件的大小不能为0,open指定的权限不能与prot参数有冲突
                            prot:PROT_READ   open:读/读写
                            prot:PROT_READ|PROT_WRITE  open:读写
                   -offset:偏移量,一般不用。必须指定为4k的整数倍,0表示不偏移
            -返回值:成功返回创建的内存的首地址
                    失败返回MAP_FAILED,(实质上是(void*)-1)

       int munmap(void *addr, size_t length);
                  -功能:释放内存映射
                  -参数:
                   -addr:要释放的内存的首地址
                   -length:要释放的内存的大小,要和mmap函数中的length一样
*/

/*
   使用内存映射实现进程间通信:
   1.有关系的进程(父子进程)
      -还没有子进程的时候
        -通过唯一的父进程,先创建内存映射区
      -有了内存映射区以后,创建子进程
      -父子进程共享创建的内存映射区

  2.没有关系的进程间通信
      -准备一个大小不是0的磁盘文件
      -进程1 通过磁盘文件创建内存映射区
          -得到一个操作这块内存的指针
      -进程2 通过磁盘文件创建内存映射区
          -得到一个操作这块内存的指针
      -使用内存映射区通信

  主要:内存映射区通信,是非阻塞的。
*/

#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <wait.h>


int main() {

    // 1.打开一个文件
    int fd = open("test.txt", O_RDWR);
    int size = lseek(fd, 0, SEEK_END);  // 获取文件的大小

    // 2.创建内存映射区
    void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if(ptr == MAP_FAILED) {
        perror("mmap");
        exit(0);
    }

    // 3.创建子进程
    pid_t pid = fork();
    if(pid > 0) {
        wait(NULL);
        // 父进程
        char buf[64];
        strcpy(buf, (char *)ptr);
        //strcpy()函数:是将一个字符串复制到另一块空间地址中 的函数
        //char* strcpy(char* destination,const char* source);
        printf("read data : %s\n", buf);
       
    }else if(pid == 0){
        // 子进程
        strcpy((char *)ptr, "nihao a, son!!!");
    }

    // 关闭内存映射区
    munmap(ptr, size);

    return 0;
}

◼ 如果对mmap的返回值(ptr)做++操作(ptr++), munmap是否能够成功?
void*ptr =mmap(...)
ptr++;可以对其进行++操作
munmap(ptr,len);  //错误,要保存地址

◼ 如果open时O_RDONLY, mmap时prot参数指定PROT_READ | PROT_WRITE会怎样?
错误,返回MAP_FAILED
open()函数中的权限建议和prot参数的权限要保持一致

◼ 如果文件偏移量为1000会怎样?
偏移量必须是4k的整数倍,返回MAP_FAILED

◼ mmap什么情况下会调用失败?
   -第二个参数:length=0
   -第三个参数:prot
          -只指定了写权限
          -prot PROT_READ|PROT_WRITE
          第五个参数fd通过open函数时指定的O_RDONLY/O_WRONLY

◼ 可以open的时候O_CREAT一个新文件来创建映射区吗?
   -可以的,但是创建的文件的大小如果为0,肯定不行
   -可以对新文件进行拓展
       -lseek()
       -truncate()
◼ mmap后关闭文件描述符,对mmap映射有没有影响?
   int fd=open("xxx");
   mmap(,,,,fd,0);
   close(fd);
   映射区还存在,创建映射区的fd被关闭,没有任何影响。

◼ 对ptr越界操作会怎样?
void *ptr=mmap(NULL,100,,,,,);
 

内存映射的例子

//使用内存映射实现文件的拷贝功能
/*
  思路:
  1.对原始文件进行内存映射
  2.创建一个新文件(拓展该文件)
  3.把新文件的数据映射到内存中
  4.通过内存拷贝将第一个文件的内存数据拷贝到新的文件内存中
  5.释放资源

*/

#include <stdio.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>


int main()
{
  //1.对原始文件进行内存映射
  int fd =open("english.txt",O_RDWR);
   if(fd==-1)
   {
    perror("open");
    exit(0);
   }

   //获取原始文件的长度
   int len=lseek(fd,0,SEEK_END);
  //2.创建一个新文件
  int fd1 =open("cpy.txt",O_RDWR|O_CREAT,0664);
  if(fd1 ==-1)
  {
    perror("open");
    exit(0);
  }

  //对新创建的文件进行拓展
   truncate("cpy.txt",len);
   write(fd1," ",1);

  //3.分别做内存映射
  void *ptr=mmap(NULL,len,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
  void *ptr1=mmap(NULL,len,PROT_READ|PROT_WRITE,MAP_SHARED,fd1,0);

  if(ptr==MAP_FAILED)
  {
    perror("mmap");
    exit(0);
  }

    if(ptr1==MAP_FAILED)
  {
    perror("mmap");
    exit(0);
  }

  //4.通过内存拷贝将第一个文件的内存数据拷贝到新的文件内存中
  //内存拷贝
  memcpy(ptr1,ptr,len);
     
  //5.释放资源
  //先创建后释放
  munmap(ptr1,len);
  munmap(ptr,len);

  close(fd1);
  close(fd);

    return 0;
}

使用匿名映射进行父子进程的通信

/*
    匿名映射:不需要文件实体进程一个内存映射
*/

#include <stdio.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>

int main() {

    // 1.创建匿名内存映射区
    int len = 4096;
    void * ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if(ptr == MAP_FAILED) {
        perror("mmap");
        exit(0);
    }

    // 父子进程间通信
    pid_t pid = fork();

    if(pid > 0) {
        // 父进程
        strcpy((char *) ptr, "hello, world");
        wait(NULL);
    }else if(pid == 0) {
        // 子进程
        sleep(1);
        printf("%s\n", (char *)ptr);
    }

    // 释放内存映射区
    int ret = munmap(ptr, len);

    if(ret == -1) {
        perror("munmap");
        exit(0);
    }
    return 0;
}

2.19信号

 

 

 

 

 

 

kill.c

/*
   #include <sys/types.h>
   #include <signal.h>

   int kill(pid_t pid, int sig);
   -功能:给任何进程或者进程组pid,发送任何的信号sig
   -参数:
       -pid:
       >0:将信号发送给指定的进程
       =0:将信号发送给当前的进程组
       =-1:将信号发送给每一个有权限接收这个信号的进程
       <-1:这个pid=某个进程组的ID取反(-12345)

       -sig:需要发送的信号的编号或者宏值,表示不发送任何信号
       -sig:需要发送信号的编号或者宏值

      kill(getppid(),9);
      kill(getpid,9);//强制杀死当前进程

 int raise(int sig);
    -功能:给当前的进程发送信号
    -参数
        -sig:要发送的信号
    
    返回值:
         0:成功
         非0:失败

    kill(getpid(),sig);
    void abort(void);
    -功能:发送SIGABRT信号给当前进程,杀死当前进程
    kill(getpid(),SIGABRT)
*/

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

   int main()
   {
     pid_t pid=fork();

     if(pid==0)
     {
        //子进程
        int i=0;
        for(i=0;i<5;i++)
        {
            printf("child process\n");
            sleep(1);
        }
     }
     else if(pid>0)
     {
        //父进程
        printf("parent process\n");
        sleep(2);
        printf("kill child process\n");
        kill(pid,SIGINT);

     }


    return 0;
   }

定时器

alarm.c //只能定一次时间

/*
       #include <unistd.h>
       unsigned int alarm(unsigned int seconds);
         -功能:设置定时器(闹钟),函数调用,开始倒计时,当倒计时为0的时候,
                函数会给当前进程发送一个信号:SIGALARM
         -参数:
             seconds:倒计时时长,单位:秒。如果倒计时为0,定时器无效
                     取消一个定时器,通过alarm(0);
         -返回值:
            -之前没有定时器,返回0
            -之前有定时器,返回倒计时剩余的时间

      -SIGALARM:默认终止当前进程,每一个进程都有且只有一个定时器

      alarm(10); ->返回0
      过了一秒
      alarm(5);  ->返回9
      

      alarm(100);该函数是不阻塞的


  实际时间=内核时间+用户时间+消耗的时间
  进行文件IO操作的时候比较浪费时间

   定时器与进程的状态无关(自然定时法)

*/

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

int main()
{
    int seconds=alarm(5);
    printf("seconds=%d\n",seconds);//返回0

    sleep(2);
    seconds=alarm(2);//不阻塞
    printf("seconds=%d\n",seconds);//返回3

    while(1)
    {
      
    }
    return 0;
}

设置定时器

setitimer.c

/*
       #include <sys/time.h>
       int getitimer(int which, struct itimerval *curr_value);
       int setitimer(int which, const struct itimerval *new_value,
                     struct itimerval *old_value);
        - 功能:设置定时器(闹钟)。可以替代alarm函数。精度微妙us,可以实现周期性定时
        - 参数:
            - which : 定时器以什么时间计时
              ITIMER_REAL: 真实时间,时间到达,发送 SIGALRM   常用
              ITIMER_VIRTUAL: 用户时间,时间到达,发送 SIGVTALRM
              ITIMER_PROF: 以该进程在用户态和内核态下所消耗的时间来计算,时间到达,发送 SIGPROF

            - new_value: 设置定时器的属性
            
                struct itimerval {      // 定时器的结构体
                struct timeval it_interval;  // 每个阶段的时间,间隔时间
                struct timeval it_value;     // 延迟多长时间执行定时器
                };

                struct timeval {        // 时间的结构体
                    time_t      tv_sec;     //  秒数     
                    suseconds_t tv_usec;    //  微秒    
                };

            过10秒后,每个2秒定时一次
           
            - old_value :记录上一次的定时的时间参数,一般不使用,指定NULL
        
        - 返回值:
            成功 0
            失败 -1 并设置错误号
*/
 // 过3秒以后,每隔2秒钟定时一次  
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    struct itimerval new_value;
    
    //设置间隔的时间
    new_value.it_interval.tv_sec=2;
    new_value.it_interval.tv_usec=0;

    //设置延迟的时间
    new_value.it_value.tv_sec=3;
    new_value.it_value.tv_usec=0;

   
   int ret = setitimer(ITIMER_REAL,&new_value,NULL);//非阻塞的
   printf("定时器开始了...\n");

   if(ret==-1)
   {
    perror("setitimer");
    exit(0);
   }
     
     getchar();//获取

    return 0;
}

2.23信号捕抓函数 

signal.c

/*
       #include <signal.h>
       typedef void (*sighandler_t)(int);
       sighandler_t signal(int signum, sighandler_t handler);

        - 功能:设置某个信号的捕捉行为
        - 参数:
            - signum: 要捕捉的信号
            - handler: 捕捉到信号要如何处理
                - SIG_IGN : 忽略信号
                - SIG_DFL : 使用信号默认的行为
                - 回调函数 :  这个函数是内核调用,程序员只负责写,捕捉到信号后如何去处理信号。
                回调函数:
                    - 需要程序员实现,提前准备好的,函数的类型根据实际需求,看函数指针的定义
                    - 不是程序员调用,而是当信号产生,由内核调用
                    - 函数指针是实现回调的手段,函数实现之后,将函数名放到函数指针的位置就可以了。

        - 返回值:
            成功,返回上一次注册的信号处理函数的地址。第一次调用返回NULL
            失败,返回SIG_ERR,设置错误号
            
    SIGKILL SIGSTOP不能被捕捉,不能被忽略。

*/


#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

void myalarm(int num)
{
    printf("捕捉到了信号的编号是:%d\n", num);
    printf("xxxxxxx\n");
}

int main()
{
    //注册信号捕抓
    //signal(SIGALRM,SIG_IGN);//忽略信号
    //signal(SIGALRM,SIG_IGN);//使用信号默认行为
   // void (*sighandler_t)(int); 函数指针,int类型的参数表示捕捉到的信号的值。
    signal(SIGALRM,myalarm);//要在定时器之前


    struct itimerval new_value;
    
    //设置间隔的时间
    new_value.it_interval.tv_sec=2;
    new_value.it_interval.tv_usec=0;

    //设置延迟的时间
    new_value.it_value.tv_sec=3;
    new_value.it_value.tv_usec=0;

   
   int ret = setitimer(ITIMER_REAL,&new_value,NULL);//非阻塞的
   printf("定时器开始了...\n");

   if(ret==-1)
   {
    perror("setitimer");
    exit(0);
   }
     
     getchar();//获取

    return 0;
}   

2.24信号集

 

1.用户通过键盘  Ctrl + C, 产生2号信号SIGINT (信号被创建)

2.信号产生但是没有被处理 (未决)
    - 在内核中将所有的没有被处理的信号存储在一个集合中 (未决信号集)
    - SIGINT信号状态被存储在第二个标志位上
        - 这个标志位的值为0, 说明信号不是未决状态
        - 这个标志位的值为1, 说明信号处于未决状态
    
3.这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集),进行比较
    - 阻塞信号集默认不阻塞任何的信号
    - 如果想要阻塞某些信号需要用户调用系统的API

4.在处理的时候和阻塞信号集中的标志位进行查询,看是不是对该信号设置阻塞了
    - 如果没有阻塞,这个信号就被处理
    - 如果阻塞了,这个信号就继续处于未决状态,直到阻塞解除,这个信号就被处理

 

 sigset.c

/*
    以下信号集相关的函数都是对自定义的信号集进行操作。

    int sigemptyset(sigset_t *set);
        - 功能:清空信号集中的数据,将信号集中的所有的标志位置为0
        - 参数:set,传出参数,需要操作的信号集
        - 返回值:成功返回0, 失败返回-1

    int sigfillset(sigset_t *set);
        - 功能:将信号集中的所有的标志位置为1
        - 参数:set,传出参数,需要操作的信号集
        - 返回值:成功返回0, 失败返回-1

    int sigaddset(sigset_t *set, int signum);
        - 功能:设置信号集中的某一个信号对应的标志位为1,表示阻塞这个信号
        - 参数:
            - set:传出参数,需要操作的信号集
            - signum:需要设置阻塞的那个信号
        - 返回值:成功返回0, 失败返回-1

    int sigdelset(sigset_t *set, int signum);
        - 功能:设置信号集中的某一个信号对应的标志位为0,表示不阻塞这个信号
        - 参数:
            - set:传出参数,需要操作的信号集
            - signum:需要设置不阻塞的那个信号
        - 返回值:成功返回0, 失败返回-1

    int sigismember(const sigset_t *set, int signum);
        - 功能:判断某个信号是否阻塞
        - 参数:
            - set:需要操作的信号集
            - signum:需要判断的那个信号
        - 返回值:
            1 : signum被阻塞
            0 : signum不阻塞
            -1 : 失败

*/

#include <signal.h>
#include <stdio.h>

int main()
{
    //创建一个信号集
    sigset_t set;

    //清空信号
    sigemptyset(&set);

    //判断SIGINT是否在信号集set中
    int ret=sigismember(&set,SIGINT);
    if(ret==0)
    {
        printf("SIGINT不阻塞\n");
    }
    else if(ret==1)
    {
        printf("SIGINT阻塞\n");
    }

    //添加信号到信号集中,设置信号阻塞
    sigaddset(&set,SIGINT);
    sigaddset(&set,SIGQUIT);

     ret=sigismember(&set,SIGINT);
    if(ret==0)
    {
        printf("SIGINT不阻塞\n");
    }
    else if(ret==1)
    {
        printf("SIGINT阻塞\n");
    }

   ret=sigismember(&set,SIGQUIT);
    if(ret==0)
    {
        printf("SIGQUIT不阻塞\n");
    }
    else if(ret==1)
    {
        printf("SIGIQUIT阻塞\n");
    }
   
   //从信号集中删除一个信号
   sigdelset(&set,SIGQUIT);

   //判断SIGQUIT是否在信号集中
     ret=sigismember(&set,SIGQUIT);
    if(ret==0)
    {
        printf("SIGQUIT不阻塞\n");
    }
    else if(ret==1)
    {
        printf("SIGIQUIT阻塞\n");
    }
    return 0;
}

对某个信号的捕抓

sigaction.c

/*
       #include <signal.h>
       typedef void (*sighandler_t)(int);
       sighandler_t signal(int signum, sighandler_t handler);

        - 功能:设置某个信号的捕捉行为
        - 参数:
            - signum: 要捕捉的信号
            - handler: 捕捉到信号要如何处理
                - SIG_IGN : 忽略信号
                - SIG_DFL : 使用信号默认的行为
                - 回调函数 :  这个函数是内核调用,程序员只负责写,捕捉到信号后如何去处理信号。
                回调函数:
                    - 需要程序员实现,提前准备好的,函数的类型根据实际需求,看函数指针的定义
                    - 不是程序员调用,而是当信号产生,由内核调用
                    - 函数指针是实现回调的手段,函数实现之后,将函数名放到函数指针的位置就可以了。

        - 返回值:
            成功,返回上一次注册的信号处理函数的地址。第一次调用返回NULL
            失败,返回SIG_ERR,设置错误号
            
    SIGKILL SIGSTOP不能被捕捉,不能被忽略。

*/


#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

void myalarm(int num)
{
    printf("捕捉到了信号的编号是:%d\n", num);
    printf("xxxxxxx\n");
}

int main()
{
    //注册信号捕抓
    //signal(SIGALRM,SIG_IGN);//忽略信号
    //signal(SIGALRM,SIG_IGN);//使用信号默认行为
   // void (*sighandler_t)(int); 函数指针,int类型的参数表示捕捉到的信号的值。
    signal(SIGALRM,myalarm);//要在定时器之前


    struct itimerval new_value;
    
    //设置间隔的时间
    new_value.it_interval.tv_sec=2;
    new_value.it_interval.tv_usec=0;

    //设置延迟的时间
    new_value.it_value.tv_sec=3;
    new_value.it_value.tv_usec=0;

   
   int ret = setitimer(ITIMER_REAL,&new_value,NULL);//非阻塞的
   printf("定时器开始了...\n");

   if(ret==-1)
   {
    perror("setitimer");
    exit(0);
   }
     
     getchar();//获取

    return 0;
}   

 

 sigchld.c

/*
    SIGCHLD信号产生的3个条件:
        1.子进程结束
        2.子进程暂停了
        3.子进程继续运行
        都会给父进程发送该信号,父进程默认忽略该信号。
    
    使用SIGCHLD信号解决僵尸进程的问题。
*/

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
#include <sys/wait.h>

void myFun(int num) {
    printf("捕捉到的信号 :%d\n", num);
    // 回收子进程PCB的资源
    // while(1) {
    //     wait(NULL); 
    // }
    while(1) {
       int ret = waitpid(-1, NULL, WNOHANG);//WNOHANG表示不阻塞
       if(ret > 0) {
           printf("child die , pid = %d\n", ret);
       } else if(ret == 0) {
           // 说明还有子进程活着
           break;
       } else if(ret == -1) {
           // 没有子进程
           break;
       }
    }
}

int main() {

    // 提前设置好阻塞信号集,阻塞SIGCHLD,因为有可能子进程很快结束,父进程还没有注册完信号捕捉
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGCHLD);
    sigprocmask(SIG_BLOCK, &set, NULL);

    // 创建一些子进程
    pid_t pid;
    for(int i = 0; i < 20; i++) {
        pid = fork();
        if(pid == 0) {
            break;
        }
    }

    if(pid > 0) {
        // 父进程

        // 捕捉子进程死亡时发送的SIGCHLD信号
        struct sigaction act;
        act.sa_flags = 0;
        act.sa_handler = myFun;
        sigemptyset(&act.sa_mask);
        sigaction(SIGCHLD, &act, NULL);

        // 注册完信号捕捉以后,解除阻塞
        sigprocmask(SIG_UNBLOCK, &set, NULL);

        while(1) {
            printf("parent process pid : %d\n", getpid());
            sleep(2);
        }
    } else if( pid == 0) {
        // 子进程
        printf("child process pid : %d\n", getpid());
    }

    return 0;
}

2.28 共享内存

 

 

共享内存相关的函数

#include <sys/ipc.h>

#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);

    - 功能:创建一个新的共享内存段,或者获取一个既有的共享内存段的标识。

        新创建的内存段中的数据都会被初始化为0

    - 参数:

        - key : key_t类型是一个整形,通过这个找到或者创建一个共享内存。

                一般使用16进制表示,非0值

        - size: 共享内存的大小

        - shmflg: 属性

            - 访问权限

            - 附加属性:创建/判断共享内存是不是存在

                - 创建:IPC_CREAT

                - 判断共享内存是否存在: IPC_EXCL , 需要和IPC_CREAT一起使用

                    IPC_CREAT | IPC_EXCL | 0664

        - 返回值:

            失败:-1 并设置错误号

            成功:>0 返回共享内存的引用的ID,后面操作共享内存都是通过这个值。


 

void *shmat(int shmid, const void *shmaddr, int shmflg);

    - 功能:和当前的进程进行关联

    - 参数:

        - shmid : 共享内存的标识(ID),由shmget返回值获取

        - shmaddr: 申请的共享内存的起始地址,指定NULL,内核指定

        - shmflg : 对共享内存的操作

            - 读 : SHM_RDONLY, 必须要有读权限

            - 读写: 0

    - 返回值:

        成功:返回共享内存的首(起始)地址。  失败(void *) -1


 

int shmdt(const void *shmaddr);

    - 功能:解除当前进程和共享内存的关联

    - 参数:

        shmaddr:共享内存的首地址

    - 返回值:成功 0, 失败 -1

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

    - 功能:对共享内存进行操作。删除共享内存,共享内存要删除才会消失,创建共享内存的进行被销毁了对共享内存是没有任何影响。

    - 参数:

        - shmid: 共享内存的ID

        - cmd : 要做的操作

            - IPC_STAT : 获取共享内存的当前的状态

            - IPC_SET : 设置共享内存的状态

            - IPC_RMID: 标记共享内存被销毁

        - buf:需要设置或者获取的共享内存的属性信息

            - IPC_STAT : buf存储数据

            - IPC_SET : buf中需要初始化数据,设置到内核中

            - IPC_RMID : 没有用,NULL

key_t ftok(const char *pathname, int proj_id);

    - 功能:根据指定的路径名,和int值,生成一个共享内存的key

    - 参数:

        - pathname:指定一个存在的路径

            /home/nowcoder/Linux/a.txt

            /

        - proj_id: int类型的值,但是这系统调用只会使用其中的1个字节

                   范围 : 0-255  一般指定一个字符 'a'


 

问题1:操作系统如何知道一块共享内存被多少个进程关联?

    - 共享内存维护了一个结构体struct shmid_ds 这个结构体中有一个成员 shm_nattch

    - shm_nattach 记录了关联的进程个数

问题2:可不可以对共享内存进行多次删除 shmctl

    - 可以的

    - 因为shmctl 标记删除共享内存,不是直接删除

    - 什么时候真正删除呢?

        当和共享内存关联的进程数为0的时候,就真正被删除

    - 当共享内存的key为0的时候,表示共享内存被标记删除了

        如果一个进程和共享内存取消关联,那么这个进程就不能继续操作这个共享内存。也不能进行关联。

    共享内存和内存映射的区别

    1.共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外)

    2.共享内存效率更高

    3.内存

        所有的进程操作的是同一块共享内存。

        内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存。

    4.数据安全

        - 进程突然退出

            共享内存还存在

            内存映射区消失

        - 运行进程的电脑死机,宕机了

            数据存在在共享内存中,没有了

            内存映射区的数据 ,由于磁盘文件中的数据还在,所以内存映射区的数据还存在。

    5.生命周期

        - 内存映射区:进程退出,内存映射区销毁

        - 共享内存:进程退出,共享内存还在,标记删除(所有的关联的进程数为0),或者关机

            如果一个进程退出,会自动和共享内存进行取消关联。

2.30守护进程 

 

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值