Linux多进程开发

一  概念

1 程序

2 进程

区别:程序是文件,占用磁盘的大小,不占用系统其他的资源(内存),进程占用内存和CPU资源 

3 单道程序设计

4 多道程序设计

5 时间片

6 并行

7 并发

 

 一般来说,并行不太会发生,因为需要耗费很多CPU,成本高

8 进程控制块PCB

有时候也称:进程描述符表

指令: ulimit -a   可以查看资源的上限

如果想要修改open files 默认的大小1024 修改为 ulimit -n 修改数

二 进程状态转换

1 进程的状态

注意:阻塞态必须要先转成就绪态,和进程抢CPU的时间片,才能变成运行态

新建:刚刚建立进程

2 进程相关命令

1  查看进程

ps  报告当前进程的信息

ps  aux

PID        进程ID 

%CPU   CPU使用率

TTY       当前进程所属终端

STAT     状态

ps  ajx

PPID   父进程的ID

PGID    父组进程ID

SID       会话ID

 2 实时显示进程动态

退出  q

3 杀死进程

3 进程号和相关函数

获取父进程ID   getppid()

获取子进程ID    getpid()

三  进程创建

1 创建进程   fork

一个终端也是一个进程

例子:

2 父子进程虚拟地址空间情况

fork函数只会在父进程中使用,,子进程中是不会使用的

子进程相当于拷贝父进程

在父子进程中他们是在单独的内存空间运行,在调用fork的时候他们的内容是一样的

结果:主要增加了num进行验证,子进程和父进程是分开运行的,互相都不影响。在初始化的时候都是10,子进程拷贝了父进程的num,但是后来对父进程+10操作,对子进程+100操作都不影响。

注意:读时共享,写时拷贝

不是完全的拷贝,禁止浪费资源

3 父子进程间的关系

4 GDB多进程调试

hello.c程序

GDB调试  -g

进入GDB调试  gdb 可执行程序

查看程序代码  l

在第十行父进程打一个断点 b 10

在第二十行子进程打一个断点 b 20

查看断点信息   i  b

运行程序  run

可以设置调试父进程或者子进程 默认父进程

1  默认情况下,GDB追踪父进程,子进程直接运行完

2 如果修改默认为子进程,那么会停在子进程,父进程运行完

设置调试模式   set detach-on-fork   在fork的时候脱离GDB调试  默认on

四 exec函数族

1 概念

函数族:一系列函数名称不同,但是完成的功能是相同和相似的  excute 执行

注意:fork函数可以产生一个子进程,但是exec不能产生子进程,只能把原来的程序替换掉,但是一般来说这样是不可行的,因为exec直接把正在执行的主程序代码替换掉,更合理的解决方法是,正在执行的程序可以先fork一个子进程,再子进程中调用exec,这样就把子进程中的程序替换称exce所指定的可执行程序内容,因为当前进程也在执行自己功能。

为什么exec执行成功不会返回:因为exec执行成功说明原来进程被替换掉了

2 exec函数族图解

内核区:维护当前进程的相关信息:进程ID 父进程ID 当前进程的状态PCB

图解:如果选择调用exec函数族指定执行a.out可执行程序,那么原来进程中的内核区仍然不变,只是把原来用户区的内容换成a.out的用户去内容,类似金蝉脱壳,躯壳没变(内核区),到那时灵魂改变(用户区)

3 exec函数族

execl execlp 用的最多  属于标准C库的函数 man 3 函数名

返回值的解释:因为excel被调用成功,他的用户区数据被替换成a.out的数据,所以没有excel的返回值,如果调用失败,美誉被替换,还是excel的返回值

案例:excel

1 将hello.c生成可执行程序hello

2 主程序

3 excel生成可执行程序 运行

验证:在父进程中执行父进程中的printf和pid,execl调用成功所以不执行子进程中的printf,但是会执行父进程中的共享代码i循环,但是最后子进程输出了hello里面的内容“我是何雨欣”(执行hello可执行程序的代码)

但是会发现没有打印在一起:孤儿进程问题(sleep可以解决) 暂停一秒

发现打印在一起并且出现在父子进程共享i循环之前

目前运行的是自己的可执行程序,如果想要执行Linux操作系统的可执行程序呢?        

ps在Linux中也是一个可执行程序:查看位置 which ps

其实这句话和 ps aux 的作用一样(查看进程)注意路径写法

执行了 ps aux 查看所有进程

案例: excelp

也能执行ps aux (不需要写绝对路径,execlp后面写文件名就可以)

因为execlp能从环境变量中查找ps

注意:如果把Linux中的ps换成自己定义的hello 是否可以用execlp 文件名?

不可以!因为hello在环境变量中找不到

进程创建:fork   exec函数族

fork作用:减少内存的浪费 节约拷贝的时间

五 进程控制

1 进程退出

标准C库函数

Linux系统函数

案例:exit()

案例  _exit()

区别:

发现编译输出结果:为什么一个打印hello world 一个打印hello?

答:因为第一个printf带有\n,他有自动刷新IO缓冲区的能力,第二个printf是先把内容hello写到缓冲区调用系统exit,world在缓冲区但是没有刷新,不显示

简记:exit会刷新缓冲区,_exit不会刷新缓冲区

退出进程:exit,一般不用_exit

2 孤儿进程

案例:

pid 子进程   ppid 父进程

创造孤儿进程:先让父进程执行完,子进程sleep,就会产生先后顺序

会发现:执行完父进程后进入终端再去打印子进程而且pid=1,由进程1回收孤儿进程的遗留问题

3 僵尸进程

子进程都死了,父进程没有回收他的资源

案例:父进程一直循环打印,没有死,但是子进程死了

defunct 不存在

S sleep

Z 僵尸进程

问题:父进程一直循环

解决方法:杀死kill无效   Ctrl +C 杀死进程(实际开发中不可行)需要waitpid  和wait 进行释放

4 进程回收

进程回收函数:wait    waitpid

1 wait函数   

查看文档    man 2 wait

易错:

分析:pid_t创建了一个父进程,父进程执行过程中遇到fork生成一个子进程,两次fork两个子进程,第一个子进程向下执行遇见fork再产生子进程,一共有四个进程:一个父进程,两个子进程,一个孙子进程,所以不可以用5次fork调用产生5个子进程

解决方法:

案例:

运行结果分析:

产生了5个子进程,后来子进程死掉了,一直重复打印父进程,五个子进程成为僵尸进程

通过复制会话查看ps aux    25-29

可以利用Ctrl +C 关闭父进程(被进程号为1的接收),这时候就不会产生僵尸进程了

更改:

子进程:1233446 1233447 1233448 1233449 1233450

父进程:1233445

父进程打印了一次:因为阻塞在wait()函数

杀掉子进程1233448   kill -9 1233448

显示被杀死1233448,说明父进程执行了wait函数,不再阻塞了,回收打印被回收子进程id

wait一次只能回收一个,所以while循环也是为了多次回收

五个子进程被杀死之后,返回的ret为-1,可以优化代码

优化代码:ret=-1

2 waitpid函数

注意:表示还有子进程活着     pid=-1最常用

案例:阻塞 0

头文件:

主函数:

测试结果分析:因为设置了阻塞,所以waitpid下面代码都不可被执行,如果kill之后会显示被杀死等信息,如果设置非阻塞WNOHANG则会向下执行代码

非阻塞的好处:父进程不用在waitpid这个地方挂起,可以继续向下执行,之后再回收

案例:非阻塞NOHANG

#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>  //fork函数需要
#include <stdio.h>   //标准C库
#include <stdlib.h>  //exit头文件

int main()
{
       //1 有一个父进程,想要创建五个子进程(兄弟关系) fork创建
       pid_t pid; //创建一个父进程
       
       //2 创建五个子进程
       for(int i=0;i<5;i++)
       {
              pid=fork();  //如果fork返回值为0则时子进程
              if(pid==0)
              {
                 break;//如果是子进程则不会向下执行产生孙子进程
              }
       }

       //3 pid判断
       if(pid>0)
       {
              //父进程
              while(1) //加入while死循环是为了一直让父进程不结束,产生僵尸进程
              {
                     printf("我是父进程,pid= %d\n",getpid());
                     sleep(1);//打印速度慢一点
                    int st;
             //pid=-1回收所有子进程,st指针类型,0设置阻塞和wait一样,WNOHANG非阻塞
                    //int ret=waitpid(-1,&st,0);  阻塞
                    int ret=waitpid(-1,&st,WNOHANG);//非阻塞
                     //成功时返回子进程id
                     if(ret==-1)
                     {
                    //返回-1的时候就会停止运行
                       break;
                     }
                     else if(ret==0)
                     {
                     //说明还有子进程存在
                        continue;
                     }
                     else if(ret>0)
                     {
                     //说明回收了子进程,返回子进程的id,判断子进程的状态
                        //是不是正常退出
                        if(WIFEXITED(st))
                        {
                        printf("退出的状态码:%d\n",WEXITSTATUS(st));
                        }
                        //是不是异常那个终止
                        if(WIFSIGNALED(st))
                        {

                        printf("被哪个信号干掉了:%d\n",WTERMSIG(st));
                        }               
                        printf("子进程死了,pid=%d\n",ret);
                        
                     } 
                    
              }
       }
       else if(pid==0)
       {
              //子进程
              while(1)
              {
                      printf("我是子进程,pid= %d\n",getpid());
                      sleep(1);
              }
               exit(0);//数字决定退出的状态码,0是正常退出
            
       }
       else if(pid==-1)
       {
              //fork失败
              printf("fork失败!");
       }
       return 0;
}

运行结果:

分析:如果是阻塞的父进程不会循环打印,因为停留在waitpid

5 退出信息相关宏函数

也就是不再使用wait(NULL)

案例1:WIFEXITED(st)

头文件:

主函数:

结果:

案例2 :WIFSIGNALED(st)

利用信号杀死

子进程:1234175 1234176 1234177 1234178 1234179

父进程:1234174

kill -9 1234177  子进程1234177被9信号杀死

六 进程间通信(面试重点)

1 进程间通讯的概念

进程是一个独立的资源分配单元,不同进程(这里所说的进程通常指的是用户进程)之间的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源。但是,进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信(IPC:Inter Processes Communication)。

GUI 用户图形接口

IDE 集成开发环境

API 应用程序接口

IPC 进程间通信

2 进程间通信的目的

数据传输:一个进程需要将它的数据发送给另一个进程 (不是孤立的)
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)
资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供互斥同步机制。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

区分同步和异步:

同步:大家一起去看医生,一个接一个(保证隐私性)

异步:大家一起去看医生(产生隐私安全问题)

进程异步会带来问题:比如进程A写数据,进程B读数据,A写数据的时候,B读到的数据是实时变化的,产生数据安全问题

3 Linux进程间通信的方式

Socket 套接字

4 匿名管道

注意:一个命令对应一个进程

文件描述符前三个被占用:标准输入、标准输出、标准错误

1 管道的特点

区分字节流和消息:

读取一条消息里面有很多数据,但是管道只能一次读一个字节

单工:遥控器发射数据给电视,单向的,电视不能发消息给遥控器

双工:两人之间同时打电话,相互发送数据,双车道

半双工:同一个时间,数据只能往一个方向,单车道,对讲机(我发的时候对方只能听,不能发送)管道也是半双工,单向

为什么匿名管道不能使用lseek函数随机访问?

答:因为管道的数据一旦被读取就被释放,所以不可随机访问

为什么匿名管道只能在具有亲缘关系的进程间使用呢?

答:共享文件描述符

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

答:因为父进程fork出来的子进程的文件描述符是共享的,所以可进行进程间的通信。

2 管道的数据结构

管道的数据结构模型:环形队列(循环队列)

数组是一个单端队列,指针读完之后被删除的空间就不能再使用了,但是循环队列写指针从末尾开始写,读指针从头部开始读取。当首末重合的时候,可以将以前被删除的部分覆盖重新写入,线性队列就会造成资源的浪费。

3 匿名管道的使用

1 创建匿名管道

2 查看管道缓冲大小命令

pipesize  8块   8*512=4096=4k

3 查看管道缓冲大小函数

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

管道应该在fork之前创建:否则父子进程就不是同一个管道了,会得到读写两个文件描述符

案例1:子进程发送一次数据,父进程接收一次数据

#include <unistd.h> //管道函数
#include <sys/types.h>//fork函数
#include <stdio.h>
#include <stdlib.h>
#include <string.h> //strlen函数

//子进程发送数据给父进程,父进程读取到数据输出
int main()
{
    //1 在fork之前创建管道
    int pipefd[2];//读写两个
    int ret=pipe(pipefd);//创建管道函数,int类型 int pipe(数组),用ret接收

    //ret判断
    if(ret==-1)
    {
        //pipe报错
        perror("pipe");
        //退出程序
        exit(0);
    }
    //2 利用fork创建父进程
    pid_t pid=fork();
    if(pid>0)
    {
        //父进程--接收数据-从管道的读取端读取数据(涉及read函数,读取成功返回字节长度)
        char buf[1024]={0};//数组初始化
        int len=read(pipefd[0],buf,sizeof(buf));
        printf("父进程读到的数据:%s,pid:%d\n",buf,getpid());
    }
    else if(pid==0)
    {
        //子进程--发送数据--从管道写入端写数据(涉及write函数)
        char *str="上岸!";
        write(pipefd[1],str,strlen(str));
    }
    else if(pid==-1)
    {
        printf("fork失败!");
    }
    return 0;
}

read函数默认是阻塞的,只有有数据的时候,才会读取

举一反三:

如果在子进程加了sleep(10),那么read刚开始没有读取到数据阻塞了,后来10s之后开始显示读取的数据。

注意:管道是默认阻塞的,如果管道中没有数据那么read会阻塞,如果管道满了write会阻塞。

案例2:子进程不断的发送数据,父进程不断的接收数据  while循环

#include <unistd.h> //管道函数
#include <sys/types.h>//fork函数
#include <stdio.h>
#include <stdlib.h>
#include <string.h> //strlen函数

//子进程发送数据给父进程,父进程读取到数据输出
int main()
{
    //1 在fork之前创建管道
    int pipefd[2];//读写两个
    int ret=pipe(pipefd);//创建管道函数,int类型 int pipe(数组),用ret接收

    //ret判断
    if(ret==-1)
    {
        //pipe报错
        perror("pipe");
        //退出程序
        exit(0);
    }
    //2 利用fork创建父进程
    pid_t pid=fork();
    if(pid>0)
    {
        //父进程--接收数据-从管道的读取端读取数据(涉及read函数,读取成功返回字节长度)
        char buf[1024]={0};//数组初始化
        while(1)
        {
          int len=read(pipefd[0],buf,sizeof(buf));
          printf("父进程读到的数据:%s,pid:%d\n",buf,getpid());
        }
    }
    else if(pid==0)
    {
        //子进程--发送数据--从管道写入端写数据(涉及write函数)
        printf("我是子进程,pid:%d\n",getpid());
        while(1)
        {
        char *str="上岸!";
        write(pipefd[1],str,strlen(str));
        sleep(1);//每写一次停留一会儿,读的时候不用,因为没有数据会阻塞
        }

    }
    else if(pid==-1)
    {
        printf("fork失败!");
    }
    return 0;
}

运行结果:

案例3:相互之前发送接收数据,子进程发送一次,父进程写一次然后发送一次,子进程接收数据再发送一次

注意:父进程和子进程一定都不能刚开始都是读的状态,两者都阻塞,程序失败

#include <unistd.h> //管道函数
#include <sys/types.h>//fork函数
#include <stdio.h>
#include <stdlib.h>
#include <string.h> //strlen函数

//子进程发送数据给父进程,父进程读取到数据输出
int main()
{
    //1 在fork之前创建管道
    int pipefd[2];//读写两个
    int ret=pipe(pipefd);//创建管道函数,int类型 int pipe(数组),用ret接收

    //ret判断
    if(ret==-1)
    {
        //pipe报错
        perror("pipe");
        //退出程序
        exit(0);
    }
    //2 利用fork创建父进程
    pid_t pid=fork();
    if(pid>0)
    {
        //父进程
        char buf[1024]={0};//数组初始化
        while(1)
        {
          //1 接收数据-从管道的读取端读取数据(涉及read函数,读取成功返回字节长度)
          int len=read(pipefd[0],buf,sizeof(buf));
          printf("父进程读到的数据:%s,pid:%d\n",buf,getpid());

          //2 写数据
          char *str="上岸!我是子进程";
          write(pipefd[1],str,strlen(str));
          sleep(1);//每写一次停留一会儿,读的时候不用,因为没有数据会阻塞

        }
    }
    else if(pid==0)
    {
        //子进程--发送数据--从管道写入端写数据(涉及write函数)
        printf("我是子进程,pid:%d\n",getpid());
        char buf[1024]={0};//数组初始化

        while(1)
        {
         //1 写数据
         char *str="上岸!";
         write(pipefd[1],str,strlen(str));
         sleep(1);//每写一次停留一会儿,读的时候不用,因为没有数据会阻塞

         //2 读数据
          int len=read(pipefd[0],buf,sizeof(buf));
          printf("子进程读到的数据:%s,pid:%d\n",buf,getpid());


        }

    }
    else if(pid==-1)
    {
        printf("fork失败!");
    }
    return 0;
}

运行结果:父子读写交替

案例:管道的大小

#include <unistd.h> //管道函数
#include <sys/types.h>//fork函数
#include <stdio.h>
#include <stdlib.h>
#include <string.h> //strlen函数

int main()
{
    // 1 创建管道两个读写数组
    int pipefd[2];

    //2 创建管道
    int ret=pipe(pipefd);

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


}

运行结果:

5 匿名管道通信案例

一定要加sleep(1),否则会出现子进程写入然后又被子进程读取的情况

6 管道读写特点

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

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

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

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

总结:

7 设置管道非阻塞

本质:设置文件描述符非阻塞

5 有名管道

1  概念

注意:

1 七种文件系统,其中一种就是管道文件

2 FIFO可以进程父子进程间通信

3 FIFO first in first out

2 有名管道和匿名管道的区分

注意:

1 FIFO文件是有文件的实体,但是文件里面是没有内容的,放在内存里面(缓冲区,程序结束就被释放了),pipe文件是没有实体的

2 不相关的进程可以通过有名管道通信,但是匿名管道只可以通过匿名管道通信

3 有名管道的使用

注意:

1 unlink 删除

2 lseek文件定位操作,FIFO管道不支持,因为读完数据就返回了,不存在了

案例:创建FIFO文件

方法一:利用命令创建FIFO文件

注意:

1 echo 写数据   echo “Hello World!”>> fifo

出现阻塞情况

发现写入的数据并没有占用内存大小,因为FIFO写入的数据放在内存中,相当于一个缓冲区,程序结束就被释放了

2 fifo 管道符号

方法二:利用函数创建FIFO文件
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>


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

        //创建一个fifo文件 名字+权限(八进制的数)
        ret = mkfifo("fifo1",0664);
        if(ret==-1)
        {
            perror("mkfifo");
            exit(0);
        }
    return 0;
    }


}

运行结果: 创建出fifo1管道文件

案例:利用有名管道实现两个进程之间的通信

思路:一个write进程往管道中写数据,另外一个read进程从管道中读数据

写进程

//向管道中写数据

#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 ret=access("test",F_OK);
    if(ret==-1)
    {
        printf("管道不存在,创建管道\n");

     //2 创建一个fifo文件 名字+权限(八进制的数)
        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);
    }

    //4 写数据
    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);
    }
    //5 关闭管道
    close(fd);

    return 0;
}

读进程

//从管道中读取数据

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

int main()
{
    // 1 以只读的方式打开管道
    int fd=open("test",O_RDONLY);
    if (fd==-1)
    {
        perror("open");
        exit(0);       
    }
    //2 循环读数据
    while(1)
    {
        char buf[1024]={0};
        int len=read(fd,buf,sizeof(buf));
        if(len==0)
        {
            printf("写端断开连接了...\n");
            break;//不可以使用exit(0),否则下面一句话不能执行
        }
        printf("接收到的数据:%s\n",buf);
    }
    //3 关闭管道文件
    close(fd);
    return 0;
}

验证两个进程的可行性

发现管道不存在,创建管道,阻塞了,因为没有写入数据(读端没有打开),如果写入数据会打印

验证通过之后,要利用两个会话进行进程间的通信

write中显示创建管道,但是只要read写端一开放就有数据了

数据写完之后,断开连接

如果读端已经关闭了,那么写端会终止程序,因为产生信号SIGPIPE信号

有名管道的注意事项:

1 一个为只读而打开一个管道的进程会阻塞,直到另外一个进程为只写打开管道

2 一个为只写而打开一个管道的进程会阻塞,知道另外一个进程为只读打开管道

有名管道实现简单版聊天功能:

 UserA:

#include <stdio.h>
#include <unistd.h>  //accsess
#include <sys/types.h>//mkfifo.open
#include <sys/stat.h>//mkfifo.open
#include <fcntl.h>//open
#include <string.h>//memset
#include <stdlib.h>


int main()
{
    //1 判断有名管道fifo1是否存在,若不存在则创建
    int ret=access("fifo1",F_OK);
    if(ret==-1)
    {
        //文件不存在
        printf("管道1不存在,需要创建对应管道");

        //mf创建管道(名字+权限)
        int ret=mkfifo("fifo1",0664);

        //如果ret还没创建成功,报出错误号
        if(ret==-1)
        {
            perror("mkfifo");
            //0为正常退出,负数为异常退出
            exit(0);
        }
    }

    //2 判断有名管道fifo2是否存在,若不存在则创建
    int ret=access("fifo2",F_OK);
    if(ret==-1)
    {
        //文件不存在
        printf("管道2不存在,需要创建对应管道");

        //mf创建管道(名字+权限)
        int ret=mkfifo("fifo2",0664);

        //如果ret还没创建成功,报出错误号
        if(ret==-1)
        {
            perror("mkfifo");
            //0为正常退出,负数为异常退出
            exit(0);
        }
    }

    //3 以只写的方式打开fifo1     open("文件名",读写权限);返回文件描述符
    int fdw = open("fifo1",O_WRONLY);
    if(fdw==-1)
    {
        perror("open");
        exit(0);
    }
    printf("打开管道fifo1成功,等待写入数据\n");//为了输出这句话应该前面exit改为break,但是不在while循环中,break不可用

    //4 以只读的方式打开fifo2     open("文件名",读写权限);返回文件描述符
    int fdr = open("fifo2",O_RDONLY);
    if(fdr==-1)
    {
        perror("open");
        exit(0);
    }
    printf("打开管道fifo2成功,等待读取数据\n");//为了输出这句话应该前面exit改为break

    char buf[128];

    //5 循环写读数据
    while(1)
    {
        //利用memset清空数组内的数据(数组名,0,数组大小)因为每次获取新数据要给之前数据清空
        memset(buf,0,128);
        
        //获取标准输入的数据(不可使用scanf因为遇到换行就终止)
        fgets(buf,128,stdin);//fgets(数组,大小,获取指针stdin)

        //写数据 write(文件描述符,写入的数组,写入数组大小) 返回int类型
        int ret = write(fdw,buf,strlen(buf));
        if(ret==-1)
        {
          perror("write");
          exit(0);
        }
    //6 读数据
    memset(buf,0,128);
    int ret = read(fdr,buf,strlen(buf));
       //ret=-1调用失败,ret=0说明对面吧管道关闭了相当于读到管道的末尾
        if(ret<=0)
        {
            perror("read");
            break;
        }
        printf("读到的数据是:%s/n");
    }

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

 UserB:

#include <stdio.h>
#include <unistd.h>  //accsess
#include <sys/types.h>//mkfifo.open
#include <sys/stat.h>//mkfifo.open
#include <fcntl.h>//open
#include <string.h>//memset
#include <stdlib.h>


int main()
{
    //1 判断有名管道fifo1是否存在,若不存在则创建
    int ret=access("fifo1",F_OK);
    if(ret==-1)
    {
        //文件不存在
        printf("管道1不存在,需要创建对应管道");

        //mf创建管道(名字+权限)
        int ret=mkfifo("fifo1",0664);

        //如果ret还没创建成功,报出错误号
        if(ret==-1)
        {
            perror("mkfifo");
            //0为正常退出,负数为异常退出
            exit(0);
        }
    }

    //2 判断有名管道fifo2是否存在,若不存在则创建
    int ret=access("fifo2",F_OK);
    if(ret==-1)
    {
        //文件不存在
        printf("管道2不存在,需要创建对应管道");

        //mf创建管道(名字+权限)
        int ret=mkfifo("fifo2",0664);

        //如果ret还没创建成功,报出错误号
        if(ret==-1)
        {
            perror("mkfifo");
            //0为正常退出,负数为异常退出
            exit(0);
        }
    }

    //3 以只读的方式打开fifo1     open("文件名",读写权限);返回文件描述符
    int fdr = open("fifo1",O_RDONLY);
    if(fdr==-1)
    {
        perror("open");
        exit(0);
    }
    printf("打开管道fifo1成功,等待读取数据\n");//为了输出这句话应该前面exit改为break,但是不在while循环中,break不可用

    //4 以只写的方式打开fifo2     open("文件名",读写权限);返回文件描述符
    int fdw = open("fifo2",O_WRONLY);
    if(fdw==-1)
    {
        perror("open");
        exit(0);
    }
    printf("打开管道fifo2成功,等待写入数据\n");//为了输出这句话应该前面exit改为break

    char buf[128];

    //5 循环读写数据
    while(1)
    {
    memset(buf,0,128);
    int ret = read(fdr,buf,strlen(buf));
       //ret=-1调用失败,ret=0说明对面吧管道关闭了相当于读到管道的末尾
        if(ret<=0)
        {
            perror("read");
            break;
        }
        printf("读到的数据是:%s/n");

        }
    //6 写数据
        //利用memset清空数组内的数据(数组名,0,数组大小)因为每次获取新数据要给之前数据清空
        memset(buf,0,128);
        
        //获取标准输入的数据(不可使用scanf因为遇到换行就终止)
        fgets(buf,128,stdin);//fgets(数组,大小,获取指针stdin)

        //写数据 write(文件描述符,写入的数组,写入数组大小) 返回int类型
        int ret = write(fdw,buf,strlen(buf));
        if(ret==-1)
        {
          perror("write");
          exit(0);
    }

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

调试结果:

6 内存映射

也是进程间通信的一种方式,效率比较高

1 概念

 意思:从文件偏移量off的位置开始映射指定长度len的映射内存

映射到动态库中

off 偏移量   offset

len 映射长度

2 内存映射相关系统调用

注意:

m memmory 

map 映射

unmap 释放映射

函数1   映射到内存中  mmap

 函数2  释放内存映射

注意:管道是阻塞的

案例:两个有关系的父子进程利用内存映射实现进程间的通信

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


int main()
{
    // 1 打开一个文件
    int fd = open("test.txt",O_RDWR);
    int size = lseek(fd,0,SEEK_END);//从末尾偏移0=文件的大小
    
    //2 利用mmap函数创建内存映射区(首地址NULL,映射内存大小=文件的大小,读写权限和open对应,同步或非同步,文件描述符,偏移量),返回一个指针
    void *ptr = mmap(NULL,size,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
    if(ptr==MAP_FAILED)//将-1转化成指针类型
    {
        perror("mmap");
        exit(0);
    }

    //3 fork创建子进程(共享父进程的内存映射区)
    pid_t pid = fork();
    if(pid>0)
    {
        wait(NULL);// 父进程调用wait去回收子进程(阻塞)

        //父进程+读数据
        char buf[64];
        strcpy(buf,(char*)ptr);
        printf("子进程独到的数据:%s\n",buf);
    }
    else if(pid == 0)
    {
        //子进程+写数据
        strcpy((char *)ptr ,"你好,子进程");   //直接利用strcpy操作这块内存(char*类型地址,写入的数据)
    }

    //4 利用munmap关闭内存映射区(指针,大小)
    munmap(ptr,size);

    return 0;
}

运行结果:

案例:两个没有关系的进程利用内存映射实现进程间的通信(拆成连两个文件读写)

3 思考问题

内存映射不仅可以实现进程间的通信还可以完成文件的复制功能

/*
    使用内存映射实现文件拷贝的功能

    1 对原始的文件进程内存的映射mmap
    2 创建一个新的文件(大小0=>拓展,否则映射出错)
    3 把新文件的数据映射到内存中(分别做内存映射)
    4 通过内存拷贝将一个文件的数据拷贝到新的文件中
    5 释放资源
    6 关闭文件描述符
*/

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




int main()
{
    //1 对原始的文件进程内存的映射
    int fd = open("text.txt",O_RDWR);//利用open函数打开文件,得到一个文件描述符
    if(fd==-1)
    {
        //打开失败,提示open错误
        perror("open");
        exit(0);
    }
    //利用lseek获取原始文件的大小(文件描述符,偏移量,文件末尾)
     int len = lseek(fd,0,SEEK_END);

    //2 创建一个新的文件(大小0=>拓展,否则映射出错)
    int fd1 = open("cpy.txt",O_RDWR | O_CREAT,0664);//open创建需要加O_CREAT
    if(fd1==-1)
    {
        perror("open");
        exit(0);
    }


        //对新创建的文件拓展truncate/lseek(名称,拓展多大=原始文件成都一样)
         truncate("cpy.txt",len);

         //拓展完要利用write进行写操作(文件描述符,写的内容,大小)
         write(fd1," ",1);

    //3 把新文件的数据映射到内存中(分别做内存映射)mmap
   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将一个文件的数据拷贝到新的文件中(往哪拷贝的地址,从哪拷贝地址,拷贝长度)
    memcpy(ptr1,ptr,len);

    //5 释放资源munmap(指针,长度)
    munmap(ptr,len);
    munmap(ptr1,len);

    //6 关闭文件描述符(注意顺序:先打开的后释放,因为可能有依赖关系)
    close(fd1);
    close(fd);

    return 0;
}

运行结果:

生成了copy.txt文件,和text文件大小一样,因为从text拷贝过来(前提,文件不能太大)

4 匿名映射

案例:匿名映射实现父子进程间的通信

/*
    匿名映射:不需要文件实体进程的一个内存映射
    功能:可以实现父子进程间的通信(共享)

*/

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


int main()
{
    //1 创建匿名内存映射区
    int len = 4096;//=4k
    void *ptr = mmap(NULL,len,PROT_READ | PROT_WRITE,MAP_SHARED | MAP_ANONYMOUS,-1,0);//匿名映射中fd=-1
    if(ptr==MAP_SHARED)
    {
        perror("mmap");
        exit(0);
    }
    //2 父子进程间的通信
    pid_t pid=fork();//fork创建父子进程
    if(pid>0)
    {
        //父进程+写数据
        strcpy((char*)ptr,"你好,子进程");//ptr是void类型指针,需要强制转换
        wait(NULL);//wait回收子进程

    }
    else if(pid==0)
    {
        //子进程+读数据开始
        sleep(1);//因为内存映射是非阻塞的,不然父进程还没执行,子进程就执行完了,父进程就读不到数据了
        printf("子进程读到的数据是:%s\n",(char *)ptr);   
    }

    //3 释放内存映射区
    int ret = munmap(ptr,len);
    if(ret==-1)
    {
        perror("munmap");
        exit(0);
    }

    return 0;
}

运行结果:

等待1s,显示读取的数据

7 信号(面试)

1 信号的概念

总的62个(32 33没有)

2 Linux信号一览表

3 信号的五种默认处理动作

案例:CORE

#include <stdio.h>
#include <string.h>

int main()
{
    char *buf;//指针
    strcpy(buf,"hello");
    return 0;
}

运行结果:

错误类型:段错误(核心已转储)

原因:程序进行了char *buf非法内存的访问,而且进行操作,所以没有生成core文件

提问:为什么gcc core.c 而不是 gcc core.c -o core?

答:你自己-o指定了编译后生成的可执行文件是core,和默认core文件冲突了啊

解决方法:更改core file size的值     ulimit -c 1024  或者unlimited

注意:不能生成core文件的原因是,因为我是普通用户,没有权限,需要加sudo

其实core文件里面存储的是错误的信息

gdb调试:gdb a.out

展示错误信息:

4 信号的相关函数

1 kill函数

2 raise函数

3 abort函数

调试部分:

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

int main()
{
    //1 利用fork创建子进程
    pid_t pid = fork();

    //2 判断pid
    if(pid==0)
    {
        //子进程
        int i=0;
        for(i=0;i<5;i++)
        {
            printf("我是子进程%d\n",i);
            sleep(1);//休息1s
        }

    }
    else if(pid>0)
    {
        //父进程
        printf("我是父进程\n");
        sleep(2);
    }
    return 0;
}

运行结果:

分析:首先父进程开始执行,睡眠2s,期间执行子进程(问题)

案例:Kill

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

int main()
{
    //1 利用fork创建子进程
    pid_t pid = fork();

    //2 判断pid
    if(pid==0)
    {
        //子进程
        int i=0;
        for(i=0;i<5;i++)
        {
            printf("我是子进程%d\n",i);
            sleep(1);//休息1s
        }

    }
    else if(pid>0)
    {
        //父进程
        printf("我是父进程\n");
        sleep(2);
        printf("现在杀掉子进程!\n");
        kill(pid,SIGINT);
    }
    return 0;
}

运行结果:

分析:首先父进程开始执行,睡眠2s,期间执行子进程两次,后来执行父进程也就是kill子进程

4 alarm函数

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

int main()
{
    int seconds = alarm(5);
    printf("倒计时:%ds\n",seconds);//返回0,因为执行完alarm

    sleep(2);//睡眠2s,alarm执行2s

    seconds = alarm(3);
    printf("倒计时:%ds\n",seconds);//返回3,因为返回上一个定时器剩余的时间5-2=3,无阻塞

    while(1)
    {
        printf("笨笨早点好起来!\n");
    }

    return 0;
}

运行结果:

案例:利用定时器实现1s电脑能数多少个数?

//利用定时器实现1s电脑能数多少个数?

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

int main()
{
    alarm(1);
    int i=0;

    while(1)
    {
        printf("%i\n",i++);
    }
    return 0;
}

运行结果:

终端:163376

下载到文件中:1723354

产生差别的原因:往终端输出内容是需要耗费磁盘IO资源的,而下载到文件中只需要调用一次,所以相同的时间下,会产生很大的差别。

5 setitimer函数----周期性定时

案例:3s后,按间隔2s定时一次

alarm和setitimer的区别:

alarm只能定时一次,setitimer可以周期性定时

/*
    #include <sys/time.h>
    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 并设置错误号
*/

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

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

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

 运行结果:

目前还没有做到,每隔2s一次定时,只是发送信号中断了,后续会用到信号捕捉

可以再获得信号之后执行其他程序,不让他退出程序,所以要对信号进行捕捉

6 signal信号捕捉函数

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

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

运行结果:

忽略  键入等待

默认 终止进程

设置回调函数

一定要在设置定时器之前注册捕捉信号,不然就会出错

5 信号集及相关函数

简言之:对多个信号进行管理

PCB 进程控制块

阻塞信号集:阻塞信号递达(被处理)

未决信号集:记录所有没有递达的信号

 信号集函数

 

 

 

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

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

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

    // 从信号集中删除一个信号
    sigdelset(&set, SIGQUIT);

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

    return 0;
}

运行结果:

 6 sigprocmask函数使用

系统调用

案例:把所有的常规信号1-31的未决状态打印到屏幕

因为信号打印的很快,可能一会就被处理 ,那怎么办?

可以设置某些信号是阻塞的(未决状态),通过键盘产生这些信号

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

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

int main() {

    // 设置2、3号信号阻塞
    sigset_t set;
    sigemptyset(&set);
    // 将2号和3号信号添加到信号集中
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGQUIT);

    // 修改内核中的阻塞信号集
    sigprocmask(SIG_BLOCK, &set, NULL);

    int num = 0;

    while(1) {
        num++;
        // 获取当前的未决信号集的数据
        sigset_t pendingset;
        sigemptyset(&pendingset);
        sigpending(&pendingset);

        // 遍历前32位
        for(int i = 1; i <= 31; i++) {
            if(sigismember(&pendingset, i) == 1) {
                printf("1");
            }else if(sigismember(&pendingset, i) == 0) {
                printf("0");
            }else {
                perror("sigismember");
                exit(0);
            }
        }

        printf("\n");
        sleep(1);
        if(num == 10) {
            // 解除阻塞
            sigprocmask(SIG_UNBLOCK, &set, NULL);
        }

    }


    return 0;
}

运行结果:

打印32位信号状态

按下ctrl +C 2号信号变成未决状态

按下ctrl +\ 3号信号变成未决状态

这就是一个前台指令,输入其他指令无效,因为当前进程占用了此终端

只有通过kill -9 杀死此进程

前台进程变成后台进程&

 通过fg切换到前台进程

加上此代码 当num为10的时候就会变成非阻塞,2号信号会执行终止进程的行为

 7 sigaction信号捕捉函数

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

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

运行结果:

每隔2s打印一次

8 内核实现信号捕捉的过程

9 SIGCHILD信号

作用:针对僵尸进程有效

如果子进程执行完毕之后,父进程没有对子进程进行回收,就会产生僵尸进程,所以之前我们学了wait函数进行子进程的回收,但是wait函数只能回收一次并且是阻塞的,父进程一直在循环等待回收,但是父进程也有自己的事情需要去做,所以wait函数并不是最好的回收方式,采用SIGCHILD信号会更加有效的解决这个问题(可以对父进程的SIGCHILD信号进行捕捉,等捕捉到该信号说明子进程执行完了一次,那么再让父进程去调用wait函数进行回收,回收完父进程可以继续做自己的事情)

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

 运行结果:

8 共享内存

共享内存是进程间通信效率最高的通信方式(因为直接操作内存)

共享内存>内存映射(关联了一个文件,通过文件进行操作,然后把文件中的内容同步给内存)

1 概念

2 共享内存使用步骤

3 共享内存操作函数

4 共享内存操作命令

9 守护进程

1 终端

查看终端设备

 

查看当前进程的进程号

 2 进程组(shell作业)

3 会话

4 进程组、会话、控制终端之间的关系

5 进程组、会话操作函数

6 守护进程

7 守护进程的创建步骤

/*
    写一个守护进程,每隔2s获取一下系统时间,将这个时间写入到磁盘文件中。
*/

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

void work(int num) {
    // 捕捉到信号之后,获取系统时间,写入磁盘文件
    time_t tm = time(NULL);
    struct tm * loc = localtime(&tm);
    // char buf[1024];

    // sprintf(buf, "%d-%d-%d %d:%d:%d\n",loc->tm_year,loc->tm_mon
    // ,loc->tm_mday, loc->tm_hour, loc->tm_min, loc->tm_sec);

    // printf("%s\n", buf);

    char * str = asctime(loc);
    int fd = open("time.txt", O_RDWR | O_CREAT | O_APPEND, 0664);
    write(fd ,str, strlen(str));
    close(fd);
}

int main() {

    // 1.创建子进程,退出父进程
    pid_t pid = fork();

    if(pid > 0) {
        exit(0);
    }

    // 2.将子进程重新创建一个会话
    setsid();

    // 3.设置掩码
    umask(022);

    // 4.更改工作目录
    chdir("/home/nowcoder/");

    // 5. 关闭、重定向文件描述符
    int fd = open("/dev/null", O_RDWR);
    dup2(fd, STDIN_FILENO);
    dup2(fd, STDOUT_FILENO);
    dup2(fd, STDERR_FILENO);

    // 6.业务逻辑

    // 捕捉定时信号
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = work;
    sigemptyset(&act.sa_mask);
    sigaction(SIGALRM, &act, NULL);

    struct itimerval val;
    val.it_value.tv_sec = 2;
    val.it_value.tv_usec = 0;
    val.it_interval.tv_sec = 2;
    val.it_interval.tv_usec = 0;

    // 创建定时器
    setitimer(ITIMER_REAL, &val, NULL);

    // 不让进程结束
    while(1) {
        sleep(10);
    }

    return 0;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值