Linux C/C++学习之多进程开发

进程概述

程序和进程

Linux C/C++学习之多进程开发_第1张图片
Linux C/C++学习之多进程开发_第2张图片

单道和多道程序设计

Linux C/C++学习之多进程开发_第3张图片

时间片

Linux C/C++学习之多进程开发_第4张图片

并行与并发

Linux C/C++学习之多进程开发_第5张图片

进程控制块 PCB

Linux C/C++学习之多进程开发_第6张图片
Linux C/C++学习之多进程开发_第7张图片

进程状态转换

进程状态模型

  • 三态模型
    在这里插入图片描述
  • 五态模型
    Linux C/C++学习之多进程开发_第8张图片

进程相关命令

Linux C/C++学习之多进程开发_第9张图片

STAT 参数意义

Linux C/C++学习之多进程开发_第10张图片

Linux C/C++学习之多进程开发_第11张图片

实时显示进程动态

Linux C/C++学习之多进程开发_第12张图片

杀死进程

Linux C/C++学习之多进程开发_第13张图片

进程号、进程组

Linux C/C++学习之多进程开发_第14张图片

进程的创建

fork函数的使用

  • fork函数作用
    函数的作用:用于创建子进程。
    返回值:
    1. fork()的返回值会返回两次。一次是在父进程中,一次是在子进程中。
    2. 在父进程中返回创建的子进程的ID,
    3. 在子进程中返回0
    4. 如何区分父进程和子进程:通过fork的返回值。
    5. 在父进程中返回-1,表示创建子进程失败,并且设置errno
  • fork调用之后创建了一个子进程(即子进程中不会出现fork的调用)
  • 子进程与父进程互不干扰
#include 
#include 
#include 

int main() {

    int num = 10;

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

    // 判断是父进程还是子进程
    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;  //父进程+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循环
    for(int i = 0; i < 3; i++) {
        printf("i : %d , pid : %d\n", i , getpid());
        sleep(1);
    }

    return 0;
}

对于创建子进程后底层的理解

Linux C/C++学习之多进程开发_第15张图片

  • 实际上,更准确来说,Linux 的 fork() 使用是通过写时拷贝(copy- on-write) 实现。写时拷贝是一种可以推迟甚至避免拷贝数据的技术。内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。只用在需要写入的时候才会复制地址空间,从而使各个进程拥有各自的地址空间。例如,父子进程共用一块地址空间 原先num = 10,父进程+1 ,子进程+2 ,内核会在物理空间分别开辟一个新的空间存储。父子进程的虚拟地址空间指向各自的物理内存。
  • 也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享。
  • 注意:fork之后父子进程共享文件,fork产生的子进程与父进程相同的文件文件描述符指向相同的文件表,引用计数增加,共享文件偏移指针。

fork()函数 总结

不同点

  • fork()后
    父进程返回 pid > 0
    子进程返回 0
  • 进程中的数据
    pid:当前进程的id
    ppid:指当前进程的父进程id

共同点

在某一状态下,如子进程刚被创建出来

  • 用户数据区域相同
  • 文件描述符表相同

父子进程的变量共享情况

  • 刚开始变量内存的地址是共享的,修改后就不是了。
  • 读时共享,写时拷贝(拷贝当前变量,开辟一个新的内存,写入新变量)

GDB多进程调试

  • GDB默认跟踪父进程进行调试,(遇到父进程进入的判断语句中的断点会作用,而子进程的判断语句断点不会作用),可在GDB调试界面输入 set follow-fork-mode (child / parent )更改。
  • 使进程脱离GDB调试detach inferiors id 不是直接中断程序,而是直接将该进程的程序执行完。
  • 可通过info inferiors查看调试的进程 。开头有" * " 表示当前调试的进程
  • gdb 8.0以上的版本 使用 set detach-on-fork 有bug。需要8.0一下的版本,可以下Ubuntu 16 解决。
    Linux C/C++学习之多进程开发_第16张图片

execl 函数族

Linux C/C++学习之多进程开发_第17张图片

execl

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

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

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

execlp

 #include 
    int execlp(const char *file, const char *arg, ... );
  • 会到环境变量中查找指定的可执行文件,如果找到了就执行,找不到就执行不成功。

  • 参数:

    1. file:需要执行的可执行文件的文件名
      如: a.out 、 ps

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

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

execv

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

execve

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

envp: 是当前可执行文件时会在当前提供的环境变量中("/home/nowcoder"等)查找。

进程退出函数、孤儿进程、僵尸进程

exit 与 _exit 区别

Linux C/C++学习之多进程开发_第18张图片

  • 代码理解 区别
    注意 exit()与_exit() 代码注释
#include
#include  //exit(int val)
#include //_exit(int val)
int main() {

    printf("hello\n"); // '\n' 有刷新缓冲区的作用,使得hello 输出
    printf("world");  // 没有‘\n’ 没有刷新缓冲区的作用 使得word 还存在于缓冲区

    // exit(0); //存在刷新缓冲区的步骤,word 有输出。
    _exit(0); //没有刷新缓冲区的步骤,word还在缓冲区中没有输出
    
    return 0;
}

孤儿进程(无危害,由pid=1的进程回收)

Linux C/C++学习之多进程开发_第19张图片

僵尸进程(有危害)

Linux C/C++学习之多进程开发_第20张图片

父进程wait和waitpid的调用

  • 区别(wait会阻塞,waitpid可设置不阻塞)
  • 一个wait或waitpid 只能杀死一个进程,用循环杀死多个。
    Linux C/C++学习之多进程开发_第21张图片

wait 调用

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

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

退出信息相关宏函数(参考2.8代码)

Linux C/C++学习之多进程开发_第22张图片

Linux C/C++学习之多进程开发_第23张图片

调试完整代码 wait的传入指针 、宏函数

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); //0为状态码,通常用零表示正常结束
    }

    return 0; // exit(0)
}

waitpid调用

 #include 
    #include 
    pid_t waitpid(pid_t pid, int *wstatus, int options);
    功能:回收指定进程号的子进程,可以设置是否阻塞。
    参数:
  • pid:
    pid > 0 : 某个的子进程的pid
    pid = 0 : 回收当前进程组的所有子进程
    pid = -1 : 回收所有的子进程,相当于 wait() (最常用)
    pid < -1 : 某个进程组的组id的绝对值,回收指定进程组中的子进程
  • wstatus
    进程退出时的状态信息,传入的是一个int类型的地址,传出参数。
  • options:设置阻塞或者非阻塞。(设为阻塞时当子进程未结束则父进程不往下执行)
    0 : 阻塞
    WNOHANG : 非阻塞
  • 返回值:
    > 0 : 返回被回收的子进程的id
    = 0 : options=WNOHANG, 表示还有子进程运行
    = -1 :错误,或者没有子进程了

进程间通信

概念

Linux C/C++学习之多进程开发_第24张图片

进程间通信方式

管道(匿名,有名)、 信号(量)、消息队列、共享内存、套接字
Linux C/C++学习之多进程开发_第25张图片

匿名管道

Linux C/C++学习之多进程开发_第26张图片

  • 特点
    Linux C/C++学习之多进程开发_第27张图片
    Linux C/C++学习之多进程开发_第28张图片

为什么可以使用管道进行进程间通信

Linux C/C++学习之多进程开发_第29张图片

管道数据结构

Linux C/C++学习之多进程开发_第30张图片
注意:读取后目标数据后,管道中的对应内存被释放,供后续写入数据使用。

管道的使用

Linux C/C++学习之多进程开发_第31张图片

pipe函数

#include 

       int pipe(int pipefd[2]);
  • 参数
    pipefd [0] :指管道的读端
    pipefd [1] :指管道的写端
  • 返回值
    0 成功
    -1 失败
  • 注意
    若管道中没有数据是默认阻塞的:管道中没有数据 read阻塞 ,管道满了 write 阻塞。

测试代码

#include 
#include
#include
#include 

int main(){
    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)); //若管道中没有数据默认阻塞 read 是阻塞的!
        printf("parent recv : %s,pid : %d\n",buf,getpid());
    } else if(pid == 0) {
        //子进程 写
       // sleep(10); //测试read 阻塞
        const char * str = "I am child process";
        write(pipefd[1],str,strlen(str));
    }
    return 0;
}

查看管道缓冲大小 可用命令或者函数

控制端输入ulimit -a
或者 用fpathconf 函数 参数用 宏_PC_PIPE_BUF

#include 
#include 
#include 
#include 
#include 

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

防止父子进程自己读写同一个信息

应在父 / 子进程中关闭写 close(pipefd[1]);/ 读 close(pipefd[0]);
简单来说就是一个进程读一个进程写。

匿名管道进程间通信 案例

重点内容注意

  • STDIN_FILENO:接收键盘的输入
  • STDOUT_FILENO:向屏幕输出 ,属于文件描述符
  • wait 函数
  • dup2 函数
  • bzero函数
  • execl族 函数
//匿名管道通信案例
#include  //exit()
#include  //pipe
#include   
#include
#include //fork()
#include
int main() {
    //创建一个管道
    int pipefd[2];
    int ret = pipe(pipefd);
    if(ret == -1){
        perror("pipe");
        return 0;
    }
    //创建子进程
    pid_t pid = fork();
    if(pid > 0) {
        //父进程
        //关闭写端
        close(pipefd[1]);
        //读
        char buf[1024];
        int len = -1;
        while ((len = read(pipefd[0],buf,sizeof(buf) - 1))>0)
        {
            //过滤
            printf("%s",buf);
            bzero(buf,1024);  //buf 清空 read 变为阻塞状态
        }
        
        wait(NULL); //回收子进程
        
    } else if(pid == 0) {
        //子进程
        //关闭读端
        close(pipefd[0]);
        //重定向
        dup2(pipefd[1],STDOUT_FILENO); // STDIN_FILENO:接收键盘的输入 
                                        //STDOUT_FILENO:向屏幕输出 ,属于文件描述符
        //执行 ps aux
        while (1)
        {
            execlp("ps","ps","aux",NULL);   //若管道满了 则当前写操作阻塞
            perror("execl");
        }
        
        
        
        exit(0);
    } else {
        perror("fork");
        exit(0);
    }
    return 0;
}

管道的特点与设置管道为非阻塞出现的现象

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

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

  • 设置管道为非阻塞
    重点提醒

    1. O_NONBLOCK表示非阻塞
    2. fcntl的用法
      #include 
#include 
#include 
#include 
#include 
#include 
/*
    设置管道非阻塞
    int flags = fcntl(fd[0], F_GETFL);  // 获取原来的flag
    flags |= O_NONBLOCK;            // 修改flag的值
    fcntl(fd[0], F_SETFL, flags);   // 设置新的flag
*/
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)); //输出-1 表示没有数据
            printf("len : %d\n", len);
            printf("parent recv : %s, pid : %d\n", buf, getpid());
            memset(buf, 0, 1024);
            sleep(1);
        }

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



实验现象
Linux C/C++学习之多进程开发_第32张图片

可以发现设置管道读端为非阻塞,当没有管道没有数据时,read不阻塞 继续运行 返回-1。buf中没有数据,输出为空。

有名管道介绍及使用

概念

Linux C/C++学习之多进程开发_第33张图片
Linux C/C++学习之多进程开发_第34张图片

有名管道使用

Linux C/C++学习之多进程开发_第35张图片

#include 
#include 

       int mkfifo(const char *pathname, mode_t mode);

  • 参数解释
  1. pathname:管道名称
  2. mode :跟open函数差不多,如(O_RDONLY、O_WRONLY、O_RDWR或者 八进制数字 0664)
  • 有名管道的注意事项:
    1. 一个为只读而打开一个管道的进程会阻塞,直到另外一个进程为只写打开管道
    2. 一个为只写而打开一个管道的进程会阻塞,直到另外一个进程为只读打开管道

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

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

有名管道实现简易聊天

Linux C/C++学习之多进程开发_第36张图片

#include
#include 
#include 
#include 
#include 
#include
#include

int main() {
    //检查有名管道是否存在,不存在则创建 mkfifo()
    int ret = access("fifo1",F_OK);
    if(ret == -1) {
        printf("有名管道fifo1不存在,正在创建。。\n");
        ret = mkfifo("fifo1",0664);
        if(ret == -1) {
            perror("fifo1");
            exit(0);
        }
    }
    ret = access("fifo2",F_OK);
    if(ret == -1) {
        printf("有名管道fifo2不存在,正在创建。。\n");
        ret = mkfifo("fifo2",0664);
        if(ret == -1) {
            perror("fifo2");
            exit(0);
        }
    }
    //以只读的方式打开管道fifo1
    int fdr = open("fifo1",O_RDONLY);
    if(fdr == -1) {
        perror("fifo1");
        exit(0);
    }
    printf("fifo1管道打开成功!\n");
    //以只写的方式打开管道fifo2
    int fdw = open("fifo2",O_WRONLY);
    if(fdw == -1) {
        perror("fifo2");
        exit(0);
    }
    printf("fifo2管道打开成功!\n");
    //循环写读数据
    char buf[128];
    while(1) {
        
        //读管道
        memset(buf,0,128);
        read(fdr,buf,sizeof(buf));//管道有数据 返回读到的字节数。管道无数据,当写端全部关闭 返回0,当写端还没关闭则阻塞
        if(read <= 0) { 
            perror("read");
            exit(0);
        }
        printf("recv chater: %s\n",buf);
        //写数据
        memset(buf,0,128);
        fgets(buf,128,stdin);
        ret =write(fdw,buf,sizeof(buf));
        if(ret == -1) {
            perror("write");
            exit(0);
        }
    }
    //关闭文件描述符
    close(fdr);
    close(fdw);
    return 0;
}

后续改进建议:创建一个子进程去读,父进程去写,可以并发的执行读写。防止因为读阻塞了后续无法写入或者写阻塞后续无法读这两个情况。

内存映射

mmap函数以及 munmap函数介绍

#include 
    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类型的指针 (void *) -1
#include 
     int munmap(void *addr, size_t length);

功能:释放内存映射
参数:

  • addr : 要释放的内存的首地址
  • length : 要释放的内存的大小,要和mmap函数中的length参数的值一样。

使用内存映射实现进程间通信

实验源码在linux下的lesson21

  1. 有关系的进程(父子进程)
    - 还没有子进程的时候
    - 通过唯一的父进程,先创建内存映射区
    - 有了内存映射区以后,创建子进程
    - 父子进程共享创建的内存映射区
  • 主要思路:
    //使用内存映射实现有关系进程的通信
    //打开一个文件(已经有数据的文件 可以无后缀
    // 获取文件的大小
    // 创建内存映射区
    //判断内存映射区是否创建成功
    //创建子进程
    //父进程 读
    // 子进程 写
    // 关闭内存映射区
  1. 没有关系的进程间通信
    - 准备一个大小不是0的磁盘文件
    - 进程1 通过磁盘文件创建内存映射区
    - 得到一个操作这块内存的指针
    - 进程2 通过磁盘文件创建内存映射区
    - 得到一个操作这块内存的指针
    - 使用内存映射区通信

    注意:内存映射区通信,是非阻塞。

  • 主要思路:两个.c打开同一个文件并申请内存映射区,一个.c读,另一个.c写。最后关闭文件,关闭内存映射区

内存间拷贝案例

//创建为已经存在的文件,创建内存映射区
//新创建一个文件,然后为其拓展(使内容不为空),也创建一个内存映射区
//利用memcpy 进行内存拷贝。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include
int main() {
    //打开要复制的文件 可读可写,不能没有读权限,否则mmap调用会出错
    int fd = open("english.txt",O_RDWR);
    if(fd == -1) {
        perror("open1");
        exit(0);
    }
    //获取文件大小
    int size = lseek(fd,0,SEEK_END);
    //打开新的文件
    int fd1 = open("copy.txt",O_RDWR | O_CREAT,0664);
    if (fd1 == -1)
    {
        perror("open2");
        exit(0);
    }
    //为新文件拓展
    truncate("copy.txt",size);
    write(fd1," ",1); 
    
    //创建内存映射区
    char * ptr = mmap(NULL,size,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
    char * ptr1 = mmap(NULL,size,PROT_READ|PROT_WRITE,MAP_SHARED,fd1,0);
    //内存拷贝
    memcpy(ptr1,ptr,size);
    //关闭虚拟内存,文件
    munmap(ptr,size);
    munmap(ptr1,size);
    close(fd);
    close(fd1);
    return 0;
}

匿名映射——无需实体文件进程的内存映射

匿名映射:不需要文件实体进程一个内存映射
将MAP_ANONYMOUS传入 mmap函数 可以实现



#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

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

内存映射注意事项

  • 1.如果对mmap的返回值(ptr)做++操作(ptr++), munmap是否能够成功?
    void * ptr = mmap(…);
    ptr++; 可以对其进行++操作但是利用munmap释放内存会释放不干净(ptr已经改变)
munmap(ptr, len);   // 错误,要保存地址
  • 2.如果open时O_RDONLY, mmap时prot参数指定PROT_READ | PROT_WRITE会怎样?
    错误,返回MAP_FAILED
    open()函数中的权限建议和prot参数的权限保持一致。

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

  • 4.mmap什么情况下会调用失败?
    第二个参数:length = 0
    第三个参数:prot
    第5个参数fd 通过open函数时指定的其中一个 O_RDONLY / O_WRONLY,mmap传入 prot PROT_READ | PROT_WRITE 会出错
    总而言之,无论谁,一定要有读权限。

  • 5.可以open的时候O_CREAT一个新文件来创建映射区吗?
    可以的,但是创建的文件的大小如果为0的话,肯定不行
    可以对新的文件进行扩展
    lseek()
    truncate()

  • 6.mmap后关闭文件描述符,对mmap映射有没有影响?
    int fd = open(“XXX”);
    mmap(,fd,0);
    close(fd);
    映射区还存在,创建映射区的fd被关闭,没有任何影响。

  • 7.对ptr越界操作会怎样?
    void * ptr = mmap(NULL, 100,);
    不会只开辟100,开辟4K的整数倍。
    越界操作操作的是非法的内存 -> 段错误

信号

概念

Linux C/C++学习之多进程开发_第37张图片

  • 目的、特点、查看方式、信号种类(31个常规,其余为实时)
    Linux C/C++学习之多进程开发_第38张图片

信号一览表

Linux C/C++学习之多进程开发_第39张图片
Linux C/C++学习之多进程开发_第40张图片
注意SIGKILL 不能杀死僵尸进程(子进程中PCB资源没有被回收)。
Linux C/C++学习之多进程开发_第41张图片
Linux C/C++学习之多进程开发_第42张图片

信号5种默认处理动作

Linux C/C++学习之多进程开发_第43张图片

core文件的生成过程

  • core文件用于记录错误信息
  1. 首先创建一个.c 文件
#include
#include
int main() {
	char* buf;
	strcpy(buf,"hello world");
	return 0;
}
  1. 运行后会出错,产生命名为core的文件(需要用ulimit -a 去设置core file部分不为0),文件中保存错误信息。

  2. 在gdb调试下 输入 core-file core 会输出有关core产生原因的信息。
    Linux C/C++学习之多进程开发_第44张图片

信号相关函数调用

Linux C/C++学习之多进程开发_第45张图片

kill

#include 
#include 

    int kill(pid_t pid, int sig);
  • 功能:给任何的进程或者进程组pid, 发送任何的信号 sig
  • 参数:
    • pid :
      > 0 : 将信号发送给指定的进程
      = 0 : 将信号发送给当前的进程组
      = -1 : 将信号发送给每一个有权限接收这个信号的进程
      < -1 : 这个pid=某个进程组的ID取反 (-12345)
      - sig : 需要发送的信号的编号或者是宏值(建议用宏值,方便跨平台),0表示不发送任何信号
  • 调用方式
kill(getppid(), 9);//向父进程发送
kill(getpid(), 9);//向子进程发送

raise

#include 
#include 
 int raise(int sig);
  • 功能:给当前进程发送信号
  • 参数:
  • sig : 要发送的信号
  • 返回值:
    - 成功 0
    - 失败 非0
  • kill调用方式实现和raise相同功能
kill(getpid(), sig);   

void abort(void);

  • 功能: 发送SIGABRT信号给当前的进程,杀死当前进程
  • kill调用方式实现和abort相同功能
kill(getpid(), SIGABRT);

alarm函数

#include 
unsigned int alarm(unsigned int seconds);
  • 功能:设置定时器(闹钟)。函数调用,开始倒计时,当倒计时为0的时候,
    函数会给当前的进程发送一个信号:SIGALARM

  • 参数:
    seconds: 倒计时的时长,单位:秒。如果参数为0,定时器无效(不进行倒计时,不发信号)。
    取消一个定时器,通过alarm(0)。

  • 返回值:

  • 之前没有定时器,返回0

  • 之前有定时器,返回之前的定时器剩余的时间

  • SIGALARM :默认终止当前的进程,每一个进程都有且只有唯一的一个定时器。
    alarm(10); -> 返回0
    过了1秒
    alarm(5); -> 返回9

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

案例:电脑一秒能数多少个数

实际完成输出的时间大于1s

  • 实际的时间 = 内核时间 + 用户时间 + 消耗的时间

  • 进行文件IO操作的时候比较浪费时间

  • 定时器,与进程的状态无关(自然定时法)。无论进程处于什么状态,alarm都会计时。

setitimer

#include 
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秒定时一次,就是设置it_interval.tv_sec =2it_value.tv_sec = 10;,其中必须初始化 suseconds_t tv_usec微秒,否则是随机值,运行会出错。可以设置为0

  • old_value :记录上一次的定时的时间参数,一般不使用,指定NULL

  • 返回值:
    成功 0
    失败 -1 并设置错误号
    由于定时器到时间了,会杀死进程,故没有间隔定时的体现,下文将讲到信号捕捉函数,可以捕捉信号。

signal捕捉信号函数

#include 
    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signum, sighandler_t handler);
  • 功能:设置某个信号的捕捉行为
  • 参数:
    • signum: 要捕捉的信号
    • handler: 捕捉到信号要如何处理
      SIG_IGN : 忽略信号
      SIG_DFL : 使用信号默认的行为
    • 回调函数 : 这个函数是内核调用,程序员只负责写,捕捉到信号后如何去处理信号。void (*sighandler_t)(int);传入 int类型变量可以接受捕捉到信号的参数值

回调函数:

  • 需要程序员实现,提前准备好的,函数的类型根据实际需求,看函数指针的定义

  • 不是程序员调用,而是当信号产生,由内核调用

  • 函数指针是实现回调的手段,函数实现之后,将函数名放到函数指针的位置就可以了

  • 返回值:
    成功,返回上一次注册的信号处理函数的地址。第一次调用返回NULL
    失败,返回SIG_ERR,设置错误号

    SIGKILLSIGSTOP不能被捕捉,不能被忽略。

  • 该函数必须放到创建定时器代码的前面

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

// 过3秒以后,每隔2秒钟定时一次
int main() {

    // 注册信号捕捉
    // signal(SIGALRM, SIG_IGN);
    // signal(SIGALRM, SIG_DFL);
    // 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;

    // 设置延迟的时间,3秒之后开始第一次定时
    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;
}
运行结果

SIGALRM的编号是14
Linux C/C++学习之多进程开发_第46张图片

信号集以及相关操作函数

概念

Linux C/C++学习之多进程开发_第47张图片

  • 注意未决信号集无法统计传入信号数,只能置1 或 0;
  • 捕捉到信号后,开始处理信号就会调用回调函数,当有同样的信号被捕捉到,这个新信号会被阻塞,等回调函数执行完后,才会处理这个新信号。

数据结构示例

Linux C/C++学习之多进程开发_第48张图片
1.用户通过键盘 Ctrl + C, 产生2号信号SIGINT (信号被创建)

2.信号产生但是没有被处理 (未决)
- 在内核中将所有的没有被处理的信号存储在一个集合中 (未决信号集)
- SIGINT信号状态被存储在第二个标志位上
- 这个标志位的值为0, 说明信号不是未决状态
- 这个标志位的值为1, 说明信号处于未决状态

3.这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集),进行比较
- 阻塞信号集默认不阻塞任何的信号
- 如果想要阻塞某些信号需要用户调用系统的API

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

操作 自建信号集 的函数

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

sigemptyset

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

sigfillset

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

sigaddset

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

sigdelset

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

sigismember

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

操作内核信号集的函数

sigprocmask

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
  • 功能:将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换)
  • 参数:
    how : 如何对内核阻塞信号集进行处理
    1. SIG_BLOCK: 将用户设置的阻塞信号集添加到内核中,内核中原来的数据不变
    假设内核中默认的阻塞信号集是mask, mask | set
    2. SIG_UNBLOCK: 根据用户设置的数据,对内核中的数据进行解除阻塞
    mask &= ~set
    3. SIG_SETMASK:覆盖内核中原来的值
  • set :已经初始化好的用户自定义的信号集
  • oldset : 保存设置之前的内核中的阻塞信号集的状态,可以是 NULL
  • 返回值:
    成功:0
    失败:-1
    设置错误号:EFAULT(地址错误)、EINVAL(值错误)

sigpending

 int sigpending(sigset_t *set);
  • 功能:获取内核中的未决信号集
  • 参数:set,传出参数,保存的是内核中的未决信号集中的信息。

有关内核操作信号集函数的操作案例

//编写一个程序,把所有的常规信号(1-31)的未决状态打印到屏幕
// 设置某些信号是阻塞的,通过键盘产生这些信号

//创建自己的信号集 提供给内核设置信号阻塞或非阻塞。 某信号设置为阻塞状态后,用户端发出信号,可以在系统内核的未决信号集中发现对应信号位置 置1
#include 
#include 
#include 

int main () {
    int num = 0;
    while(1) {
        num++;
        sigset_t set;
        //清空信号集
        sigemptyset(&set);

        //添加到信号集
        sigaddset(&set,SIGINT);
        sigaddset(&set,SIGQUIT);

        //将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换)
        sigprocmask(SIG_BLOCK,&set,NULL);

        //创建一个信号集,接收系统的未决信号集(即还没递达的信号)
        sigset_t sigpend;
        sigemptyset(&sigpend);
        sigpending(&sigpend);
        for(int i = 1; i <= 32;i++) {  //检测信号集中是否有阻塞的信号 ,有则返回1
            if(sigismember(&sigpend,i) ==1 ) {
                printf("1");
            } else if(sigismember(&sigpend,i)== 0) {
                printf("0");
            }
        }
        printf("\n");
        sleep(1);
        if(num == 10) {
            // 解除阻塞
            sigprocmask(SIG_UNBLOCK, &set, NULL);
        }
    }
    
    return 0;
}

信号捕捉函数

sigaction

#include 
    int sigaction(int signum, const struct sigaction *act,
                            struct sigaction *oldact);
  • 功能:检查或者改变信号的处理。信号捕捉

  • 参数:
    signum : 需要捕捉的信号的编号或者宏值(信号的名称)
    act :捕捉到信号之后的处理动作
    oldact : 上一次对信号捕捉相关的设置,一般不使用,传递NULL

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

  • 参数中的结构体介绍
    1.注意只有在信号捕捉函数才会使用临时阻塞信号集 sigset_t sa_mask·,捕捉函数执行完后临时阻塞信号集不再启用,会还原为内核的阻塞信号集。

  struct sigaction {
        // 函数指针,指向的函数就是信号捕捉到之后的处理函数 , int数据类型用于接收信号的对应序号
        void     (*sa_handler)(int);
        // 不常用
        void     (*sa_sigaction)(int, siginfo_t *, void *);
        // 临时阻塞信号集,在信号捕捉函数执行过程中,临时阻塞某些信号。
        sigset_t   sa_mask;
        // 使用哪一个信号处理对捕捉到的信号进行处理
        // 这个值可以是0,表示使用sa_handler,也可以是SA_SIGINFO表示使用sa_sigaction
        int        sa_flags;
        // 被废弃掉了
        void     (*sa_restorer)(void);
    };
调用案例(在signal函数演示案例上更改)
  • 注意这个如何用
    act.sa_handler = myalarm;
  • 案例代码实现
#include 
#include 
#include 
#include 

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

// 过3秒以后,每隔2秒钟定时一次
int main() {

    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = myalarm;
    sigemptyset(&act.sa_mask);  // 清空临时阻塞信号集
   
    // 注册信号捕捉
    sigaction(SIGALRM, &act, NULL);

    struct itimerval new_value;

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

    // 设置延迟的时间,3秒之后开始第一次定时
    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();
    while(1);

    return 0;
}

SIGCHILD 信号产生条件及案例演示

/*

  • SIGCHLD信号产生的3个条件:
    1.子进程结束
    2.子进程暂停了
    3.子进程继续运行
    都会给父进程发送该信号,父进程默认忽略该信号。

  • 使用SIGCHLD信号解决僵尸进程的问题。

案例演示

注意提前设置好自定义阻塞信号集的目的提前设置好阻塞信号集,阻塞SIGCHLD,因为有可能子进程很快结束,父进程还没有注册完信号捕捉

#include 
#include 
#include 
#include 
#include 
#include 

void myFun(int num) {
    printf("捕捉到的信号 :%d\n", num);
    // 回收子进程PCB的资源
    // while(1) {
    //     wait(NULL); 
    // }
    while(1) {
       int ret = waitpid(-1, NULL, 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;
}

共享内存

概念

Linux C/C++学习之多进程开发_第49张图片

共享内存使用步骤

Linux C/C++学习之多进程开发_第50张图片

共享内存相关操作函数

shmget

#include 
#include 

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,后面操作共享内存都是通过这个值。

shmat

void *shmat(int shmid, const void *shmaddr, int shmflg);
  • 功能:和当前的进程进行关联
  • 参数:
    - shmid : 共享内存的标识(ID),由shmget返回值获取
    - shmaddr: 申请的共享内存的起始地址,指定NULL,内核指定
    - shmflg : 对共享内存的操作
    - 读 : SHM_RDONLY, 必须要有读权限
    - 读写: 0
  • 返回值:
    成功:返回共享内存的首(起始)地址。
    失败: (void *) -1

shmdt

int shmdt(const void *shmaddr);
  • 功能:解除当前进程和共享内存的关联
  • 参数:
    shmaddr:共享内存的首地址
  • 返回值:成功 0, 失败 -1

shmctl

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

ftok

key_t ftok(const char *pathname, int proj_id);
  • 功能:根据指定的路径名,和int值,生成一个共享内存的key(shmget中的参数可以用这个
  • 参数:
    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),或者关机
      如果一个进程退出,会自动和共享内存进行取消关联。

守护进程

与守护进程有关的概念介绍

Linux C/C++学习之多进程开发_第51张图片

Linux C/C++学习之多进程开发_第52张图片
Linux C/C++学习之多进程开发_第53张图片
Linux C/C++学习之多进程开发_第54张图片
Linux C/C++学习之多进程开发_第55张图片

守护进程概念

Linux C/C++学习之多进程开发_第56张图片

守护进程创建步骤

  • 为什么要由子进程调用setsid()? 如果父进程调用会出现重复id的进程组。
    Linux C/C++学习之多进程开发_第57张图片

创建守护进程案例 (daemon 进程/精灵进程)

  • /* 写一个守护进程 每隔两秒获取系统时间 */
  • 仔细看注释!
/* 写一个守护进程 每隔两秒获取系统时间 */
#include
#include 
#include 
#include 
#include 
#include
#include 
#include 
#include
#include
#include
void work(int num) {
    //捕捉到信号后 ,获取系统时间 写入磁盘文件
    //printf("抓取到信号编号:%d\n",num);
    time_t  tmm = time(NULL); 
    struct tm * loc = localtime(&tmm); //定义为现在时间
    /* 写入数组的形式输出(未去重定向stdIn /OUT /ERR_FILE)才能看到*/
    // char buf[1024];
    // sprintf(buf,"%d-%d-%d  %d:%d:%d",loc->tm_year,loc->tm_mon,loc->tm_mday,loc->tm_hour,loc->tm_min,loc->tm_sec);
    // printf("%s\n",buf);

    /*写入磁盘,重定向stdIn /OUT /ERR_FILE的情况下 在磁盘文件中看到,屏幕上看不到*/
    char * ptr = asctime(loc); //返回时间信息
    int fd = open("time.txt",O_RDWR|O_CREAT|O_APPEND,0664);
    write(fd,ptr,strlen(ptr));
    close(fd);
}
int main() {
    //创建子进程
    pid_t pid = fork();
    //退出父进程
    if(pid>0) exit(0);

    //子进程调用setsid 创建会话(守护进程创建)
    setsid();
    //umask以确保守护进程创建文件和目录时有权限
    umask(664); //不能写0664 必须写三位 要不会出错!
    //修改当前进程目录 chdir
    chdir("/home/yang/");
    //关闭和重定向文件描述符
    int fd = open("/dev/null",O_RDWR);
    dup2(fd,STDERR_FILENO);
    dup2(fd,STDIN_FILENO);
    dup2(fd,STDOUT_FILENO);
    //核心业务逻辑

    //定时器计时完毕会发出SIGALARM 信号 需要去捕捉
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = work;
    sigaction(SIGALRM,&act,NULL);
    //创建定时器
    struct itimerval set;
    //第一次开始 过2秒开始运行
    set.it_interval.tv_sec = 2;
    set.it_interval.tv_usec = 0;

    //每隔两秒运行一次
    set.it_value.tv_sec = 2;
    set.it_value.tv_usec = 0;
    setitimer(ITIMER_REAL,&set,NULL);
    
    //不让进程结束,定时器才会持续工作
    while(1) {
        sleep(10);
    }
    return 0;
}

你可能感兴趣的:(Cpp,Learning,Road,linux,c++,多进程)