Linux简明教程-进程通信

9 进程间通信

9.1 绪论

进程间通信(interprocess communication,简称IPC),Linux环境下进程空间相互独立,每个进程都有各自不同用户的地址空间,任何一个进程的全局变量在另外一个进程都看不到,所以进程之间不能相互访问,想要交换数据必须通过内核,在内核中将进程1把数据从用户空间拷贝到用户缓冲区,进程2再从用户缓冲区将数据拷贝走,内核提供的这种通信叫做IPC。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8Ob77hxm-1642599432631)(/home/daniel/.config/Typora/typora-user-images/image-20210516211808686.png)]

两个进程的内存区域,除了内核之外的部分是不同的,所以只能通过内核进行通信。在内核中,存在一块缓冲区,一个进程向其中写入数据,另外一块从其中读出数据,便实现了进程间的通信。

缓冲区默认大小一般是4kB,4096B;

进程间通信方式:文件,管道,信号,共享内存,消息队列,套接字,命名管道。随着科技发展,现今常用的进程间通信方式有:

1.管道,(使用最简单)默认是命名管道,只能用于有血缘关系的进程之间,即父子进程等等;称为匿名管道

​ FIFO 系统中的管道文件,通过mkfifo创建,是有名管道,可以用于传递无血缘关系的进程通信;

2.信号,(开销最小)系统资源占用少,运行速度快

3.共享映射区,(无血缘关系)

4.本地套接字, (最稳定)应用于网络中,实现复杂度最高

9.2 管道(匿名管道)

9.2.1 管道的基本概念

管道:

1.本质是伪文件(不占用内核空间,实为内核缓冲区,可以使用mkfifo filename进行创建);

2.由两个文件描述符引用,一个表示读端,另外一个表示写端;

3.规定数据从管道的写端流入管道,从读端流出管道;

管道原理:管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现

管道局限性:

1.管道不能进程自己写,自己读;

2.管道中数据不可反复读取,一旦读走,管道中不复存在;

3.采用半双工通信方式,数据只能在单方向上流动;

常见通信方式:单工通信,半双工通信,全双工通信;

9.2.2 pipe函数

函数:int pipe(int pipefd[2])

头文件:#include <unistd.h> #include <fcntl.h>

描述:创建并打开管道(不需要单独open函数) 管道是双向半双工通信(可以双向流动,但是每次只能选择一个方向) 但是使用完成之后需要使用close回收文件描述符

pipe() creates a pipe, a unidirectional data channel that can be used for interprocess communication. The array pipefd is used to return two file descriptors referring to the ends of the pipe. pipefd[0] refers to the read end of the pipe. pipefd[1] refers to the write end of the pipe. Data written to the write end of the pipe is buffered by the kernel until it is read from the read end of the pipe. For further details, see pipe(7).

参数:fd[0] 读端

​ fd[1] 写端

​ 可以使用close函数关闭其中一个文件描述符

​ 父子进程可以共享文件描述符

返回值: 成功,返回0;失败返回-1,设置erron

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S7NFQtZq-1642599432631)(/home/daniel/.config/Typora/typora-user-images/image-20210516221731749.png)]

//demo 
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void sys_err(char* str)
{
    perror(str);
    exit(1);
}

int main(int argc, char** argv)
{
    int ret;
    //初始化文件描述符数组
    int fd[2];
    ret=pipe(fd);
    if(ret==-1){
        sys_err("create pipe error!\n");
    }
    __pid_t pid;
    pid=fork();
    if (pid>0){
        //关闭读端
        close(fd[0]);
        char* str="hello, my son\n";
        //向管道写入
        write(fd[1],str,strlen(str));
        close(fd[1]);
    }
    else if(pid==0){
        close(fd[1]);
        char* buf[1024];
        //从管道读出
        ret=read(fd[0],buf,1024);
        if(ret==-1){
            sys_err("read pipe error!\n");
        }
        write(STDOUT_FILENO,buf,ret);
        close(fd[0]);
    }
    return 0;
}

9.2.3 管道读写行为 0517

读管道:

1.管道中有数据,read实际返回读到的字节数;

2.管道中无数据:

​ 1)管道写端被全部关闭,read返回0(和读到文件末尾类似)

​ 2)管道读端没有全部被关闭,read阻塞等待(不久的将来可能有数据到达,此时回让出CPU资源)

写管道:

1.管道读端被全部关闭,进程异常终止(也可使用捕捉SIGPIPE信号,使进程不终止);

2.管道读端没有全部关闭;

1)管道已满,write阻塞;(缓冲区满了之后,内核会对缓冲区做扩容,所以比较难实现该种现象)

2)管道未满,write将数据写入,并返回实际写入的字节数;

使用管道实现 ls|wc -l命令,统计ls命令产生的行数

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

int main(int argc, char** argv)
{
    int fd[2];
    int ret1=pipe(fd);
    if(ret1<0){
        perror("pipe error!\n");
    }
    int ret=fork();
    if(ret<0){
        perror("fork error!\n");
    }
    if(ret>0){
        //sleep(1);
        //主进程是读端,负责读出数据给wc -l命令;关闭写端
        close(fd[1]);
        dup2(fd[0],STDIN_FILENO);
        //原来从用户输入读,写在变成
        execlp("wc","wc","-l",NULL);
    }
    if(ret==0){
        //子进程是写端,关闭读端;
        close(fd[0]);
        //char buf[4096];
        //ls输出到屏幕,将其重定向一个文件描述符
        dup2(fd[1],STDOUT_FILENO);
        //子进程执行  ls  原来是向屏幕写,现在变为向管道中写
        execlp("ls","ls",NULL);
        close(fd[1]);      
    }
    return 0;
}

9.2.4 兄弟进程通信

在单独使用兄弟进程进行通信时,需要关闭父进程的读端和写端;

#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
void sys_err(char *error)
{
    perror(error);
    exit(1);
}

int main(int argc, char **argv)
{
    int i = 0;
    int fd[2];
    int ret1 = pipe(fd);
    if (ret1 < 0)
    {
        sys_err("pipe error!\n");
    }
    for (i = 0; i < 2; i++) //父进程出口
    {
        if (fork() == 0)
        {
            break;      //子进程出口
        }
    }
    if (i == 0)
    {
        close(fd[0]);
        dup2(fd[1], STDOUT_FILENO);
        execlp("ls", "ls", NULL);
    }
    if (i == 1)
    {
        close(fd[1]);
        dup2(fd[0],STDIN_FILENO);
        execlp("wc", "wc", "-l", NULL);
    }
    if (i==2){
        //管道是两个子进程在通信,需要关闭父进程中的读端和写端
        close(fd[0]);
        close(fd[1]);
        while((waitpid(-1,NULL,0))!=-1){}
    }
    return 0;
}

9.2.5 多个读写端操作管道和管道缓冲区大小

管道可以有多个读端和一个写端,但是尽量避免使用该种情况,因为可能导致读写的顺序和预期的的读写顺序不一致的情况;

ulimit -a 可以查看管道的大小 也可以查看栈大小 文件描述符数量

栈的大小比较小,堆区比较大 栈的大小大概是8M heap堆大小在1.4G左右

使用库函数 long fpathconf(int fd, int name); 或者 long pathconf(char* path, int name)

可以查看文件的一些属性;

9.3 FIFO 命名(有名)管道 0518

9.3.1 创建fifo管道的方法

FIFO:创建方式

1.使用命令: mkfifo fifoname

2.使用库函数:

函数名:int mkfifo(const char* pathname, mode_t mode);

头文件:#include <sys/types.h> #include <sys/stat.h>

​ #include <fcntl.h>

参数:pathname 创建的管道文件的名字

​ mode 指定的权限,具体权限=mode& ~umask

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

描述:

   mkfifo()  makes a FIFO special file with name pathname.  mode specifies the FIFO's permissions.  It is modified by the process's umask  in  the usual way: the permissions of the created file are (mode & ~umask).A  FIFO special file is similar to a pipe, except that it is created in a different way.  Instead of being an anonymous communications channel, a FIFO special file is entered into the filesystem by calling mkfifo().
   Once  you have created a FIFO special file in this way, any process can open it for reading or writing, in the same way as  an  ordinary  file.However,  it  has to be open at both ends simultaneously before you can
   proceed to do any input or output operations on it.  Opening a FIFO for reading  normally  blocks  until some other process opens the same FIFO for writing, and vice versa.  See fifo(7) for nonblocking  handling  of
   FIFO special files.

demo:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
void sys_err(char *error)
{
    perror(error);
    exit(1);
}

int main(int argc, char** argv)
{
    int ret=mkfifo("fifo.p",0664);
    if(ret<0){
        sys_err("mkfifo error!\n");
    }
    return 0;
}

9.3.2 实现非血缘关系进程的通信

原理:一个进程向FIFO文件中写入内容,另外一个进程向FIFO文件中读出内容;

有名管道通信类似与匿名管道通信,一般是要求一个写端,一个读端;

但是管道可以有多个写端或者多个读端运行,在有多个写端的时候,读端接收到的信息的顺序是不确定的;

在有多个读端的时候,信息只有一份,也就是说多个读端共同分摊了写端输入的消息;

写入内容程序源码:

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

void sys_err(char *error)
{
    perror(error);
    exit(1);
}

int main(int argc, char** argv)
{
    if(argc!=2){
        printf("input order like: a.out pname\n");
        exit(1);
    }
    int fd=open(argv[1],O_WRONLY);
    char buf[1024];
    int i=0;
    while(1){
        sprintf(buf,"hello daniel %d\n",i++);
        write(fd,buf,strlen(buf));
        sleep(1);
    }
    return 0;    
}

读出内容源码:

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

void sys_err(char *error)
{
    perror(error);
    exit(1);
}

int main(int argc, char** argv)
{
    if(argc!=2){
        printf("input order like: a.out pname\n");
        exit(1);
    }
    int fd=open(argv[1],O_RDONLY);
    char buf[1024];
    while(1){
        int len=read(fd,buf,1024);
        write(STDOUT_FILENO,buf,len);
        sleep(1);
    }    
    return 0;
}

9.4 共享存储映射

9.4.1 文件完成进程间通信

使用文件完成进程之间的通信;使用文件完成通信既可以是父子进程之间的通信,可以是没有血缘关系的进程之间通信;能够完父子进程之间的通信,主要是因为父子进程之间能够共享文件描述符;

没有血缘关系的进程之间能够进行通信,主要是因为同一个文件在内存使用使用同一片缓冲区;

#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
#include <string.h>
void sys_err(char *error)
{
    perror(error);
    exit(1);
}

int main(int argc, char **argv)
{
    char *str="hello, It' a test file for file-connection!\n";
   int ret=fork();
   if(ret<0){
       sys_err("fork error!\n");
   }
   if(ret==0){
       int fd=open("test.txt",O_WRONLY);
       if(fd<0){
           sys_err("open file error!\n");
       }
       write(fd,str,strlen(str));
       close(fd);
   }
   else if (ret>0){
       sleep(1);
       char str1[1024];
       int fd1=open("test.txt",O_RDONLY);
       read(fd1,str1,1024);
       printf("%s",str1);
       close(fd1);
       wait(NULL);
   }
   return 0;
}

9.4.2 存储映射 I/O

存储映射I/O(Memory-mapped I/O)使一个磁盘文件与存储空间中的一个缓冲区相映射。于是当从缓冲区中取数据,就相当于读文件中的相应的字节。与此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样在不使用read和write函数的情况下,使用(地址)指针就能完成IO操作。

使用该种方法,应该首先通知内核,将一个指定文件映射到存储映射区,这个映射工作可以通过mmap函数来实现。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dsfJsShd-1642599432632)(/home/daniel/.config/Typora/typora-user-images/image-20210518210558345.png)]

函数:

   void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);

头文件:#include <sys/mman.h>

描述:

参数:addr:指定映射区的首地址,通常传入NULL;传入NULL表示让系统自动分配

int length:共享内存映射区的大小;<=文件的实际大小,通常等于文件的大小

prot:共享内存映射区的读写属性,PROT_READ 读权限PROT_WRITE写权限 读写权限将读权限和写权限位或

flags:标志共享内存的共享属性;MAP_SHARED 对内存修改反映到磁盘上 MAP_PRIVATE 对内存修改不会反映到磁盘上

fd: 用于创建共享内存映射区的那个文件 文件描述符;

offset:默认,0,表示映射文件全部;偏移位置;偏移位置必须是4k的整数倍;

返回值:

成功:返回共享内存映射区的首地址;

失败:返回MAP_FAILED,(void*)-1,设置erron

函数:int munmap(void addr, size_t length)*

头文件:#include <sys/mman.h>

描述:释放有mmap开辟的映射区

参数:void * addr mmap返回的地址

​ size_t length 地址

返回值:int 成功返回0 失败返回-1,设置erron

DEMO:

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

void sys_err(char *error)
{
    perror(error);
    exit(1);
}

int main(int argc, char** argv)
{//创建一个文件,用于创建文件映射区
    int fd=open("test.txt",O_RDWR|O_TRUNC|O_CREAT,0664);
    if (fd<0){
        sys_err("open file error!\n");
    }
    //可以使用	lseek(10,SEEK_END);write(fd,"\0",1);实现同样的效果
    int ret=ftruncate(fd,10);//拓展文件大小
    if(ret<0){
        sys_err("ftruncate error!\n");
    }//创建字符指针,用于接收mmap返回值
    char* mmap_ret;
    //使用lseek获得文件大小,作为参数传入mmap函数
    int len=lseek(fd,0,SEEK_END);
    //可读可写,并且更改会存储到硬盘
    mmap_ret=mmap(NULL,len,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
    //向指针中拷贝字符串,其实相当于写入文件
    strcpy(mmap_ret,"hello mmap!\n");
    printf("%s",mmap_ret);
    //释放mmap函数返回的指针,类似与malloc和free的关系
    munmap(mmap_ret,len);
    return 0;
}

9.4.3 mmap函数的注意事项

1.用于创建映射区的文件大小为0,实际指定映射区的大小为非0,会出现总线错误;

2.用于创建映射区文件大小为0,实际指定映射区的大小为0,会出现 无效参数错误;

3.用于创建映射区的文件读写属性为只读,映射区的属性为读/写,出现的错误是 无效参数错误;

4.创建映射区,需要读权限(read权限);mmap的读写权限,应该小于等于文件的open权限(open权限中必须要有读文件);

因为创建映射区隐含一次对文件的读操作;

5.映射区创建好之后,关闭文件描述符,映射区可以正常使用;

6.offset必须是4096的整数倍,和MMU相关,MMU映射的最小单位是4k;

7.对映射区的内存不能越界访问;

8.不能执行p++,应为这样会改变p的值,从而导致释放地址发生错误,同样也不能执行任何改变p值的操作,如果需要p邻近的内存地址,可以再定义一个自定义变量;由mmap申请的内存,需要munmap释放;

9.映射区访问权限私有,即参数是MAP_PRIVATE,对内存的所有修改只对内存有效,不会反映到磁盘上;

10.在MAP_PRIVATE时,open函数的可以是只读权限,因为MAP_PRIVATE不会改变磁盘内部的内容;

MMAP函数的一般调用方法:

int fd=open(“filename”,O_RDWR);

mmap=(NULL,有效文件大小,PROT_READ|PROT_WRITE,MAP_SHEARED,fd,0);

11.mmap函数调用之后,需要检查返回值,if (addr==MAP_FAILED)

9.4.4 父子进程间的mmap通信

父子进程之间有血缘关系进程之间也可以通过mmap建立映射区完成数据通信。但相应在父子进程之间的通信要求mmap是共享的;

MAP_SHARED:共享映射,父子进程共享映射区;

MAP_PRIVATE:私有映射,父子进程各自独占映射区;

DEMO:

#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
void sys_err(char *str)
{
    perror(str);
    exit(1);
}

int main(int argc, char** argv)
{
    int fd=open("1.txt",O_RDWR|O_CREAT|O_TRUNC,0664);
    if(fd==-1){
        sys_err("open file error\n");
    }
    ftruncate(fd,20);
    char* str_conn="hello, my father!";
    char* str_dad=NULL;
    int len=lseek(fd,0,SEEK_END);
    char* p_map=mmap(NULL,len,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
    if(p_map==MAP_FAILED){
        sys_err("map failed!\n");
    }
    close(fd);//回收文件描述符
    int ret=fork();
    if (ret<0){
        sys_err("fork error\n");
    }
    if (ret==0){
        strcpy(str_conn,p_map);
    }
    else if(ret>0){
        sleep(1);
        strcpy(p_map,str_dad);
        printf("%s\n",str_dad);
        wait(NULL);
        munmap(p_map,len);
    }
    return 0;
}

9.4.5 无血缘关系的进程使用mmap进行通信

其基本原理类似于使用文件进行通信,但是程序操作比文件更为简单,因为mmap函数直接返回可以操作字符串的指针;

两个进程打开同一个文件,创建映射区;

指定flags为MAP_SHARED

一个进程写入,另外一个进程读出;

注意:同样是无血缘关系进程通信

FIFO:数据只能读取一次;基于消息队列

mmap:数据可以重复读取;

DEMO:

//mmap_w.c	负责通过文件映射区写入数据
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
void sys_err(char *error)
{
    perror(error);
    exit(1);
}
typedef struct  stu
{
    int num;
    char name[256];
    int grade;
} stu;

int main(int argc, char** argv)
{
    
    int fd=open("test.txt",O_RDWR|O_CREAT|O_TRUNC,0664);
    if (fd<0){
        sys_err("open error!\n");
    }
    ftruncate(fd,sizeof(stu));
    stu stu1={1,"Daniel",98};
    stu* stu2=mmap(NULL,sizeof(stu),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
    if(stu2==MAP_FAILED)
        sys_err("map failed\n");
    close(fd);
    while(1){
        memcpy(stu2,&stu1,sizeof(stu));
        stu1.num++;
        sleep(1);
    }
    munmap(stu2,sizeof(stu));
    return 0;
//mmap_r.c	负责通过文件映射区读出数据
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
void sys_err(char *error)
{
    perror(error);
    exit(1);
}

typedef struct stu
{
    int num;
    char name[256];
    int grade;
} stu;

int main(int argc, char **argv)
{
    int fd = open("test.txt", O_RDWR);
    if (fd < 0)
        sys_err("open file error!\n");
    stu *stu_x;
    
    stu_x = mmap(NULL, sizeof(stu), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    if (stu_x == MAP_FAILED)
        sys_err("map failed\n");
    close(fd);

    while (1)
    {
        printf("num:%d,name:%s,grade:%d\n", stu_x->num, stu_x->name, stu_x->grade);
        sleep(1);
    }
    munmap(stu_x, sizeof(stu));
    return 0;
}

9.4.6 匿名映射

创建映射区,必须先创建一个文件;然后使用mmap;

Linux系统:

可以将flag的参数增加MAP_ANON或者增加MAP_ANONYMOUS,创建匿名映射区;文件描述符传入-1;

mmap(NULL,400,PROT_SHARED|PROT_ANON,-1,0)

类Unix系统:

老版本的unix系统没有PROT_ANON选项;

dev/zero 从其中拿数据,要拿多少有多少,读出来的数据是\0

dev/NULL 文件黑洞,可以随便往其中写东西

可以使用 int fd=open(“dev/zero”,O_RDWR);创建一个传入的文件描述符;传入的length参数随意

int fd=open("dev/zero",O_RDWR);
void* addr=mmap(NULL,length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);

匿名映射只能用于父子血缘关系进程之间的通信,而不能用于没有血缘关系的进程的通信

ANNOY或者 dev/zero创建的匿名映射,只能用于有血缘关系之间的进程通信,不能用于无血缘关系之间的进程通信;

9.5 信号 5.22

9.5.1 信号的机制和概念

信号特点:简单,无法传递复杂信息,满足某种条件才能发送

信号:进程A给进程B发送信号,B收到信号之前执行自己的代码,受到信号之后不管执行到什么位置,都要暂停运行去处理信号,处理完毕再继续执行。与硬件中断类似—异步模式,但信号是软件层面实现的,早期成为“软信号”;

信号是软件层面上的“中断”

中断:又称为时钟中断,每过一段固定的时间,都会由运行的进程切换到操作系统,是硬件实现的

每个进程收到的所有信号,都是由内核发送,内核处理

产生信号的方法:

1.按键产生;例如:ctrl + c

bg后台 fg前台 (和信号无关的两个命令)

2.系统调用产生, kill, raise,

3.软件条件产生,如:定时器 alarm C语言中的sleep函数

4.硬件异常产生,如:段错误(非法访问内存,如:修改只读,越界访问,),除0(浮点数例外),内存对齐错误(总线错误)

5.命令产生 kill命令

递达:递送并且到达进程(内核产生信号,并发送到进程的状态)

未决:信号产生和递达之间的状态,主要由于阻塞(屏蔽)导致该状态;

信号处理方式:

1.执行默认动作;

2.忽略(丢弃);

3.捕捉(调用户处理函数);

阻塞信号集(阻塞屏蔽字):将某些信号加入集合,对他们设置屏蔽,当屏蔽x信号后,再受到信号,该信号的处理将推后(接触屏蔽后);

未决信号集:

1.信号产生,未决信号集中描述该信号的位立刻翻转为1,表示信号处于未决状态,当信号被处理后,对应位翻转为0,这一时刻非常短暂;

2.信号产生后,由于某些原因(主要是阻塞)不能抵达,这类信号的集合称之为未决信号集。在屏蔽解除之前,信号一直处于未决状态;

阻塞信号集和未决信号集处于PCB控制块中,未决信号集和阻塞信号字两者的本质都是位图,

当进程收到一个信号,将先查看将未决信号集相应的位翻转位1,表示信号未被处理,如果阻塞信号字的对应位为0,表示该信号未被屏蔽(阻塞),则直接执行该信号,并且将未决信号集的对应位翻转为0;如果阻塞信号字对应的位为1,代表信号被阻塞,则只有解除阻塞之后,信号才能被执行;

9.5.2 信号四要素和常规信号一览

使用kill -l 命令在中断可以查看系统中所有信号的类型;

1~31信号,属于常规信号;由其默认的处理动作;学习重点

34~64信号,属于实时信号;编写底层的硬件驱动可能会用到,可以自定义;

daniel@daniel-Vostro-5471:~/文档/OS/test$ kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX	

变量三要素:变量类型,变量名称和变量值

信号四要素:编号,名称,事件,默认处理动作,

信号不能乱发,必须满足某种出发触发事件,了解处理动作才能发送;信号使用之前需要先确定四要素,然后再使用;

使用 man 7 signal或者查看 /usr/src/linux-headers-3.16.0-30/arch/s390/include/uapi/asm/signal.h

Linux中常规信号的介绍:

(1)SIGHUP:当用户退出shell时,由该shell启动的所有进程都会受到这个信号,默认动作为终止进程;

(2)SIGINT:当用户使用 ctrl+c 时,用户终端向正在进行中的由该终端启动的程序发出此信号,默认动作为终止该进程;

(3)SIGQUIT:当用户按下 crtl+/ 组合建之后产生该信号,用户终端向正在运行中的由该终端启动的程序发出这些信号,默认动作为终止进程;

(4)SIGILL:CPU检测到某进程执行了非法指令。默认动作终止进程,并产生core文件;

(5)SIGTRAP:该信号由断点指令或其它trap产生,默认动作时终止进程并产生core文件,和gdb调试有关;

(6)SIGABRT:调用abort函数时,产生该信号,默认动作时终止进程,并产生core文件;

(7)SIGBUS:非法访问内存地址,包括内存对其出错,默认动作为终止进程并产生core文件;

(8)SIGFPE:在发生致命的运算错误时发出,不仅包括浮点运算错误,还包括溢出以及除数为0等所有的算法错误,默认动作为终止进程,并产生core文件;

**(9)SIGKILL:**无条件终止进程,本信号不能被忽略,处理和堵塞,默认动作时终止进程,它向管理员提供了可以杀死任何进程的方法;

(10)SIGUSR1:用户定义的信号,即程序员可以在程序中定义并使用该信号,默认动作时终止进程;

(11)SIGSEGV:指示进程进程了无效内存访问,默认动作为终止进程并产生core文件;

(12)SIGUSR2:另外一个用户自定义信号,与10号信号相同

(13)SIGPIPE:Broken pipe向一个没有读端的管道写入数据,默认动作是终止进程;

(14)SIGALARM:定时器超时,超时的时间由系统调用alarm设置,默认动作是终止进程;

(15)SIGTERM:程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来表示程序的正常退出,执行shell命令kill时,缺省产生该信号,默认动作终止进程;

(16)SIGTKFLT:Linux早期版本的信号,现仍然保留向后兼容,默认动作是终止进程;

(17)SIGCHLD:子进程状态发生变化时,父进程会收到这个信号,默认动作时忽略这个信号

(18)SIGCONT:如果进程已停止,则使其继续运行,默认动作为继续/忽略信号;

(19)SIGSTOP:停止进程执行,信号不能被忽略,处理和阻塞,默认动作为暂停进程;

(20)SIGSTP:终止终端交互进程的运行,按下 ctrl+z 组合建发出这个信号,默认动作暂停进程;

(21)SIGTTIN:后台进程读终端控制台,默认动作为暂停进程;

(22)SIGTTOU:该信号类似与SIGTTIN,在后台进程要向终端输出数据发生时,默认动作为暂停进程;

(23)SIGURG:套接字上有紧急数据时,向当前正在运行的进程发出信号,报告由紧急数据送达,如网络外部数据到达,默认忽略该信号;

(24)SIGXCPU:进程执行时间超过了分配给该进程CPU的时间,系统产生该信号并发送给进程,默认动作为终止进程;

(25)SIGXFSZ:超过文件的最大长度设置,默认动作为终止进程;

(26)SIGVTALRM:虚拟时中超时时产生该信号,类似于SIGALARM,但是该信号只计算该进程占用CPU的使用时间,默认动作是终止该进程;

(27)SIGPROE:类似于SIGVTALRM,它不光包括该进程占用CPU时间,还包括执行系统调用时间,默认动作为终止进程;

(28)SIGWINCH:窗口变化大小时发出,默认动作忽略该信号;

(29)SIGIO:此信号向进程指示发出了一个异步IO事件,默认动作忽略;

(30)SIGPWR:关机,默认动作为终止进程;

(31)SIGSYS:无效的系统调用,默认动作终止进程;

(34)SIGRTMIN~(64)SIGRTMAX:LINUX实时信号,他们默认没有固定的函数,可以由用户自定义,所有的实时信号的默认动作都为终止进程;

信号的默认动作:

(1)Term:终止进程;

(2)Ign:忽略信号(对信号执行忽略操作)

(3)Core:终止进程,生成core文件

(4)Stop:停止(暂停)进程;

(5)Cont:继续运行进程;

(9)SIGKILL和(19)SIGSTOP号不能捕捉,甚至不能忽略和阻塞,只能执行默认动作;

9.5.3 产生信号

终端产生信号:

ctrl+c (2) SIGINT (终止,中断) Interrupt

ctrl+z (20)SIGTSTP(暂停,终止) Stop

ctrl+/ (3)SIGQUIT 退出

硬件异常产生信号

除0操作 (8)SIGFPE 浮点数除外

非法访问错误 (11)SIGSEGV(段错误)

总线错误 (7)SIGBUS

函数:int kill (pid_t pid, int sig)

头文件:#include <sys/types.h> #include <signal.h>

描述:man 2 kill

参数:pid_t pid 进程编号,也有其它值,比如0,-1,<-1等,具体的含义见man page pid 可以通过getpid()和getppid()函数获得;

  	   The  kill()  system  call can be used to send any signal to any process
       group or process.
       If pid is positive, then signal sig is sent to the process with the  ID
       specified by pid.
       If pid equals 0, then sig is sent to every process in the process group
       of the calling process.
       If pid equals -1, then sig is sent to every process for which the call‐
       ing  process  has  permission  to  send  signals,  except for process 1
       (init), but see below.
       If pid is less than -1, then sig  is  sent  to  every  process  in  the
       process group whose ID is -pid.
       If  sig  is  0,  then  no  signal is sent, but existence and permission
       checks are still performed; this can be used to check for the existence
       of  a  process  ID  or process group ID that the caller is permitted to
       signal.

sig 信号编号;

返回值:成功,返回0;失败返回-1,并且设置errno;

在命令行中使用:kill -9 -pid 杀死组id为pid的组内的所有进程 默认父子进程 同一组,组id为父进程的id

权限保护:super用户(root)可以发送信号给任意用户,普通用户时不能向系统用户发送信号的。kill -0 (root 用户的pid)是不可以的,同样,普通用户也不能向其它普通用户发送信号,终止其进程。只能自己创建的进程发送信号。普通用户的规则是,发送者实际或有效用户ID==接收者实际或者有效用户ID

练习:循环创建5个子进程,并用父进程杀死任意一个子进程

其它可以发送信号的函数:

int raise(int sig) 用于操作系统给调用进程或者线程发送信号,相当于 kill(getpid(),sig)

void abort(void)

9.5.4 alarm函数

设置定时器(闹钟),在指定seconds后,内核会给当前进程发送(14)SIGALRM 信号,进程收到该信号动作停止;

每个进程都有且只有唯一一个定时器

函数:unsigned int alarm (unsigned int seconds)

头文件:#include <unistd.h>

描述:使用的是自然事件,定时器不论进程运行在什么状态;

alarm()  arranges  for  a SIGALRM signal to be delivered to the calling process in seconds seconds.
If seconds is zero, any pending alarm is canceled.
In any event any previously set alarm() is canceled.

参数:unsigned int seconds 非负的整数

返回值:返回上一个定时器剩余的秒数;alarm命令没有出错的情形;

使用定时器尝试一秒钟系统能够打印多少数字:

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

int main(int argc, char** argv)
{
    int i=0;
    alarm(1);
    for(i=0;;i++){
        printf("%d\n",i);
    }
    return 0;
}

通常使用alarm(0) 取消闹钟

程序执行事件=系统事件+用户事件+等待事件

在终端中可以使用time ./pro_name 对进程运行事件进行查看;

其中在调用printf时,打印到屏幕上时相当耗时的,程序优化的关键在于优化IO操作;

9.5.6 settimer函数

函数:int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);

int getitimer(int which, struct itimerval *curr_val);

头文件:#include <sys/time.h>

描述:设置定时器(闹钟),可替代alarm函数,精度微秒,可以实现周期定时;

参数:

which:指定定时方式

(1)自然定时,ITIMER_REAL (14)SIGARLM 计算自然事件

(2)虚拟定时事件 ITIMER_VIRTUAL (26)SIGPROF 计算进程占用CPU的事件

(3)运行时计时 ITIMER_PROF (27)SIGPROF 计算占用CPU及执行系统调用的事件

new_value 定时秒数

old_value 传出参数

struct itimerval {
	struct timeval it_interval; /* Interval for periodic timer */
	struct timeval it_value;    /* Time until next expiration */
           };
struct timeval {
	time_t      tv_sec;         /* seconds */
    //微秒
	suseconds_t tv_usec;        /* microseconds */
};

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

提示:it_interval 用来设定两次定时任务之间间隔的时间;

​ it_value 定时时长

​ 两个参数都设置为0,即清0操作;

使用setitimer()函数实现循环定时的DEMO:

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

void func(int argc)
{
    printf("hello world!\n");
}

int main(int argc, char** argv)
{   
    //当检测到SIGALRM的时候,调用func函数
    signal(SIGALRM,func);
    struct itimerval new_value={
        //第一次定时器设定的时间是2s
        .it_value.tv_sec=2,
        .it_value.tv_usec=0,
        //之后循环的每次闹钟的时间是5s
        //其执行相当于while循环
        .it_interval.tv_sec=5,
        .it_interval.tv_usec=0,
    };
    struct itimerval old_value;
    setitimer(ITIMER_REAL,&new_value,&old_value);
    while(1);
    return 0;
}

9.5.7 信号集操作函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BZx2aUZj-1642599432633)(/home/daniel/.config/Typora/typora-user-images/image-20210522205401662.png)]

阻塞信号集是可以操作的,但是未决信号集不能被操作。

阻塞信号集和未决信号集的默认值均为0;

阻塞信号集设定相关的set函数,可以使用man page查看,man 3 sigsetops

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6VFMBUbA-1642599432633)(/home/daniel/.config/Typora/typora-user-images/image-20210522205845821.png)]

 Objects of type sigset_t must  be  initialized  by  a  call  to  either sigemptyset()  or  sigfillset()  before  being  passed to the functions       sigaddset(), sigdelset() and  sigismember()  or  the  additional  glibc functions  described  below  (sigisemptyset(),  sigandset(),  and  sig    orset()).  The results are undefined if this is not done.

函数:int sigprocmask(int how, const sigset_t *set, sigset_t *oldset)

头文件:#include <signal.h> 使用man 2 sigprocmask 查看帮助

描述:

  sigprocmask()  is  used  to  fetch and/or change the signal mask of the calling thread.  The signal mask is the set of signals  whose  delivery is currently  blocked  for the caller (see also signal(7) for more details).
  The behavior of the call is dependent on the value of how, as follows.
  SIG_BLOCK 
  The set of blocked signals is the union of the current  set  and the set argument.
  SIG_UNBLOCK
  The  signals  in set are removed from the current set of blocked signals. It is permissible to attempt to unblock a signal which is not blocked.
  SIG_SETMASK
  The set of blocked signals is set to the argument set.

参数:how 可以使用

​ SIG_BLOCK,设置为此,set代表需要屏蔽的信号,相当于mask=mask|set

​ SIG_UNBLOCK,设置为此,unset代表解除屏蔽的信号,相当于mask=mask&(~set)

​ SIG_SETMASK,设置为此set表示用于替代原始屏蔽集的新屏蔽集,相当于mask=set,若调用sigprocmask解除了对当前若干个信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达;

​ sigset_t *set,传入参数,传入的set状态

​ sigset_t *oldset,传出参数,之前的状态

返回值:

​ 0代表成功,-1代表失败,失败设置erron;

 		sigemptyset(), sigfillset(), sigaddset(), and sigdelset() return  0  on success and -1 on error.
       sigismember()  returns  1  if signum is a member of set, 0 if signum is
       not a member, and -1 on error.
       On error, these functions set errno to indicate the cause of the error.	

*函数:sigpending(sigset_t set)

描述:读取当前进程未决信号集

参数:sigset_t *set 传出参数

返回值:成功返回0,失败返回-1,设置erron;

#include <unistd.h>
#include <stdio.h>
#include <signal.h>
void print_set(sigset_t *pset)
{
    int i;
    for(i=1;i<32;i++){
        if (sigismember(pset,i)){
            putchar('1');
        }
        else{
            putchar('0');
        }
    }
    printf("\n");
}

int main(int argc, char** argv)
{
    sigset_t new_set;
    sigset_t old_set;
    sigset_t set_state;
    sigemptyset(&new_set);
    //SIGKILL   SIGSTOP 不能捕捉,也不能设置阻塞
    //屏蔽SIGINT信号    即crtl + c
    sigaddset(&new_set,SIGINT);
    //屏蔽定时器信号
    sigaddset(&new_set,SIGALRM);
    //虽然set中增加SIGKILL信号,但是使用kill -9 pid仍能杀死进程
    sigaddset(&new_set,SIGKILL);
    sigprocmask(SIG_BLOCK,&new_set,&old_set);
    alarm(10);
    while(1){
        sigpending(&set_state);
        print_set(&set_state);
        sleep(1);
    }     
    return 0;
}

9.5.8 注册信号捕捉函数 5.24

函数:sighandler_t signal (int signum, sighandler_t handler)

typedef void (*sighandler_t) (int);//使用sighandler作为一种变量类型,来定义函数指针

头文件:

描述:注册一个信号捕捉函数

参数:

signum 所要捕捉的信号类型

sighandler_t 函数指针,函数的类型不能随便定义,已经定义好,是返回值为void, 参数为int类型的函数

返回值:返回函数指针

DEMO:

//注册捕捉	ctrl+c	信号
#include <signal.h>
#include <stdio.h>

void catch_sig(int signo)
{
    printf("catch you! signo=%d\n",signo);
    return;
}

int main(int argc, char** argv)
{
    signal(SIGINT,catch_sig);
    while(1);
    return 0;
}

**函数:int sigaction(int signum, const struct sigaction act, struct sigaction oldact)

头文件:#include <signal.h>

描述:

struct sigaction {
    void     (*sa_handler)(int);	#函数指针
    void     (*sa_sigaction)(int, siginfo_t *, void *);//可通过信号携带复杂信息,忽略
    sigset_t   sa_mask;//只在sigaction中起作用的信号屏蔽字
    int        sa_flags;//设置屏蔽其它signum
    void     (*sa_restorer)(void);//忽略
};

参数:

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

信号捕捉特性:

1.进程正常运行,默认PCB中有一个信号屏蔽字,即mask,它决定了进程运行过程中屏蔽那些信号。当注册了某个信号捕捉函数,捕捉到该信号以后,要调用该函数。而该函数有可能执行很长事件,在这期间所屏蔽的信号不再由mask决定,而是由结构体中的sa_mask决定;

2.xxx信号捕捉函数执行期间,xxx信号自动被屏蔽;(依赖于sa_flags设置为0)

3.阻塞的常规信号不支持排队,产生多次只记录一次;(比如在阻塞期间多次发生该信号,恢复主进程运行之后,只执行一次)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m8rZDEMT-1642599432634)(/home/daniel/.config/Typora/typora-user-images/image-20210524223424248.png)]

函数需要在调用之后,返回给调用者;

9.5.9 借助SIGCHLD信号回收子进程

SIGCHLD的产生条件

子进程终止时;子进程收到SIGSTOP信号停止时;子进程处在停止态,接收到SIGCONT后唤醒时;

使用信号,在主进程中循环回收多个子进程

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

void catch_child(int signo)
{
    pid_t w_pid;
    // while((w_pid=wait(NULL))!=-1){
    //     printf("回收子进程%d成功\n",w_pid);
    // }
    while ((w_pid = waitpid(-1, NULL, 0))!=-1)//防止信号重复发送之后,僵尸进程未被回收
    {
        printf("回收子进程%d成功\n", w_pid);
    }
    return;
}

int main(int argc, char **argv)
{
    int i = 0;
    //阻塞 信号 SIGCHLD
    sigset_t block_set, old_set;
    sigemptyset(&block_set);
    sigaddset(&block_set, SIGCHLD);
    //阻塞SIG_BLOCK信号,防止出现子进程完毕,捕捉信号函数还未被注册的情况
    //这样会导致没有捕捉信号被触发
    sigprocmask(SIG_BLOCK, &block_set, &old_set);
    for (i = 0; i < 5; i++)
    {
        if (fork() == 0)
        {
            break;
        }
    }
    if (i == 5)
    {
        struct sigaction sig_a, sig_old;
        sig_a.sa_handler = catch_child;
        sigemptyset(&sig_a.sa_mask);
        sig_a.sa_flags = 0;
        sigaction(SIGCHLD, &sig_a, &sig_old);//捕捉SIGCHLD,即子进程结束信号
        //解除阻塞
        sigprocmask(SIG_UNBLOCK, &block_set, &old_set);
        printf("子进程回收完毕!\n");
        //while(1);
    }
    else
    {
        printf("I am child, my pid is %d\n", getpid());
    }
    return 0;
}

9.5.10 中断系统调用

慢速系统调用:可能会使进程永远堵塞的一类系统调用。如果在阻塞期间收到一个信号,该系统调用就会被中断,不会再被执行(早期),也可以设定系统调用是否重启,如:read,write,pause,wait,…

其它系统调用:getpid(),getppid,fork,…

结合pause,回顾慢速系统调用;

慢速系统调用被中断的相关行为,其实就是pause行为,如read

(1)想中断pause,信号不能被屏蔽;

(2)信号的处理方式必须是捕捉(默认,忽略都不可以);

(3)中断后返回-1,设置erron为EINTR(表“被信号中断”)

可以修改sa_flags来设置被信号中断后系统调用是否重启,SA_INTERRUPT不重启,SA_RESTART,重启(sigaction函数中的参数)

其中有很多可选参数,适用于不同的情况,比如捕捉到信号后,在执行捕捉函数期间,不希望自动阻塞该信号,可将sa_flags设置为SA_NODEFER,除非sa_mask中包含该信号

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值