奇妙的进程------多进程间的通信篇

前面认识进程篇已经认识了进程。

进程简单的概括就是:程序的实例化,是操作系统分配资源的基本单位。

每个进程都有自己的独立的地址空间和独立的资源。

操作系统内核会为每一个进程分配一个PCB(process control block),PCB是一个结构体,记录了进程的状态、运行时间等信息。

进程之间是相互独立的,他们之间如何通信呢?

IPC通信机制解决了进程间通信的问题

下面就IPC通信的七种基本方式做一个简单的认识介绍


目录

IPC通信机制

一、管道

二、有名管道

三、消息队列

四、磁盘映射

五、共享内存

 六、套接字

七、信号


IPC通信机制

一、管道

管道用于父子进程之间的通信。

管道又称为无名管道,是一种特殊类型的文件,在应用层上表现为两个打开的文件描述符

 无名管道的特点:

1、半双工模式,数据在同一时刻在一个方向上流动;

2、数据从fd[0]端读出,fd[1]端写入,0和1分别代表终端的标准输入和标准输出。这里形象描述;

3、写入管道的数据遵循先进先出的原则;

4、管道所传送的数据是无格式的,所以管道的读出一方和写入一方需要事先约定好数据的格式

5、管道是一种特殊的文件,不属于文件系统,他只存在内存中。

6、管道在内存中对应一个缓冲区,不同的系统缓冲区不一样大

7、从管道中读取数据是一次性读取

8、管道又称无名管道,是因为管道没有名字。

   #include <unistd.h>

       int pipe(int fildes[2]);//创建无名管道的函数

无名管道的案例:

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

int main(int argc, char const *argv[])
{
    // 创建文件描述符
    int fd[2];
    // 创建无名管道
    pipe(fd);
    // 创建父子进程
    pid_t pid = fork();
    if (pid == 0) // 子进程,功能发送数据
    {
        // 因为子进程用来发送数据,也就是向管道中写入数据,用到fd[1],没有用到读取数据一端fd[0],所以关闭fd[0]
        close(fd[0]);

        printf("子进程准备5秒后发送数据\n");
        sleep(5);
        printf("子进程发送了hello world\n");
        write(fd[1], "hello wolrd", 11);

        // 数据发送完后关闭管道写端fd[1]
        close(fd[1]);
        _exit(-1);
    }
    else if (pid > 0) // 父进程,功能接收数据
    {
        // 因为父进程用来接收数据,也就是从管道的fd[0]中读取数据,所以关闭fd[1],因为没用到
        close(fd[1]);

        printf("父进程等待来自子进程的消息\n");
        char buf[128] = "";
        read(fd[0], buf, sizeof(buf));
        printf("父进程收到了来自子进程的消息:%s\n", buf);

        // 如果不收消息关闭读端
        close(fd[0]);
        wait(NULL); // 记得回收子进程资源
        _exit(-1);
    }

    return 0;
}

 输出结果:这里五秒后输出后两段信息

通过以上案例其实可以看出管道为什么只能用于父子进程间的通信,因为管道的实现方式是基于文件描述符的,而子进程继承了来自父进程的文件描述符,因此子进程可以通过文件描述符来访问管道,管道本身是一个文件,数据通过管道传输时候,实际上数据是从一个文件描述符写入数据,然后从另一个文件描述符读取数据,也就是所谓的写端和读端,因此管道是一种单向通信机制。用于父子进程间单向通信。

无名管道的进阶案例:

//第一种复制文件描述符的方式
#include <unistd.h>
int dup(int oldfd);
功能:将已有的文件描述符oldfd 复制出一个新的文件描述符(最小可用的文件描述)
返回值:
    成功:就是新的文件描述符
    失败:-1


//第二种复制文件描述符的方式
int dup2(int oldfd, int newfd);
功能:将newfd复制成oldfd     newfd也代表oldfd代表的文件
注意:如果newfd存在 系统会自动关闭newfd 然后让newfd为oldfd的备份
//exec族函数的作用是在程序中编写代码,通过执行可执行程序启动另一个进程
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, .../* (char *) NULL */);
int execlp(const char *file,cconst char *arg, ... /* (char *) NULL */);
int execle(const char *path, const char *arg, .../*, (char *) NULL, char
* const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
int execve(const char *filename, char *const argv[], char *const en

l代表函数的参数以列表的形式存在 //l是L的小写,不是一
v代表函数的参数以指针数组的形式存在
p:表示函数从系统的path路径下寻找可执行文件
e:表示函数可以从环境变量中获取变量

在系统中通过ps -A |grep CMD 可以执行相关进程,ps -A:查看所有进程并输出到终端

|:管道

grep:全局查找CMD

ps -A | grep CMD:将ps -A查找到的所有数据输出到管道,然后grep从管道中查找CMD并输出到终端。

这里我们编写代码来实现。实现流程如下:

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

int main(int argc, char const *argv[])
{
    // 创建文件描述符
    int fd[2];
    // 创建无名管道
    pipe(fd);
    // 创建父子进程
    pid_t pid = fork();
    if (pid == 0) // 子进程,功能执行ps -A
    {
        // 因为子进程用来发送数据,也就是向管道中写入数据,用到fd[1],没有用到读取数据一端fd[0],所以关闭fd[0]
        close(fd[0]);

        printf("子进程准备5秒后发送数据\n");
        sleep(5);
        printf("子进程发送数据\n");
        dup2(fd[1],1);
        execlp("ps","ps","-A",NULL);

        // 数据发送完后关闭管道写端fd[1]
        close(fd[1]);
        _exit(-1);
    }
    else if (pid > 0) // 父进程,功能接收数据,执行grep bash
    {
        // 因为父进程用来接收数据,也就是从管道的fd[0]中读取数据,所以关闭fd[1],因为没用到
        close(fd[1]);

        
        dup2(fd[0],0);
        execlp("grep","grep","bash",NULL);
        // 如果不收消息关闭读端
        close(fd[0]);
        wait(NULL); // 记得回收子进程资源
        _exit(-1);
    }

    return 0;
}

 输出结果:

 

二、有名管道

前面讲到管道适用于父子进程间的通信,因为子进程继承了父进程的文件描述符,通过建立pipe管道,向写端写入数据,读端就可以读取数据。但是其缺点是不适用于不相关的进程间的通信。

IPC通信机制提供了另一个通信方式来解决不相关的进程间的通信--------有名管道

有名管道可以用于不相关的进程间的通信。

(注意他也可以用于同一个进程内或者父子进程间通信)

 有名管道的特点:

1、半双工模式,数据在同一时刻在一个方向上流动;

2、写入有名管道的数据遵循先进先出的原则;

3、有名管道也是无格式的,需要写入一方和读出一方事先约定好数据的格式;

4、有名管道有名字,在文件系统中作为一个特殊的文件存在,数据是存储在内存中的

5、有名管道在内存中对应一个缓冲区,不同的系统缓冲区大小不一样大;

6、从有名管道中读取数据是一次性读取;

7、当使用有名管道的进程退出之后,有名管道文件仍然继续存在,其他的进程可以通过有名管道名来打开有名管道,继续进行通信。

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo( const char *pathname, mode_t mode);
参数:
    pathname:FIFO 的路径名+文件名。
    mode:mode_t 类型的权限描述符。
返回值:
    成功:返回 0
    失败:如果文件已经存在,则会出错且返回-1

有名管道在Wfifo和Rfif0进程间通信案例:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h> //FIFO创建需要的头文件
#include <fcntl.h>    //open函数
#include <unistd.h>   //延时函数sleep、read、write
#include <string.h>   //strlen函数

int main(int argc, char const *argv[])
{
    // 在当前路径下创建一个有名管道文件,设置用户、用户组和其他用户权限为可读可写
    mkfifo("fifo", 0666);

#ifdef WRITE
    int fd = open("fifo", O_WRONLY);
#endif
#ifdef READ
    int fd = open("fifo", O_RDONLY);
#endif

    // 检测,如果有名管道文件打开失败,fd<0
    if (fd < 0)
    {
        perror("open");
        return 0;
    }
    printf("fifo文件打开成功\n");

#ifdef WRITE
    printf("3秒后发送信息\n");
    sleep(3);
    write(fd, "hello world", strlen("hello world"));
    while (1)
        ;
#endif

#ifdef READ
    char buf[128] = "";
    read(fd, buf, sizeof(buf));
    printf("从有名管道读取到的数据为:%s\n", buf);
    getchar();
#endif

    close(fd);
    return 0;
}

输出结果:

这里只是创建了两个可执行程序,为了进一步证明是不相关进程,使用命令ps -a查看

我们知道每一个进程都有唯一的进程号,通过ps -a命令 可以查看到进程Wfifo的PID是7472,进程Rfifo的PID是7611。

为了进一步证明不是父子进程,使用认识进程篇讲过的命令ps -ajx |grep Wfifo or Rfifo命令来详细显示各自进程的父进程号

结果:

ps -ajx | grep Wfifo

//分别输入两个命令
ps -ajx | grep Rfifo

 

 因为开了两个终端和一个grep,所以我们只需要对照上面的ps -a找到对应的PID,可以看到Wfifo进程的PPID是7471,而Rfifo的PPID是7610,所以两个是不相关的进程。也进一步的证明了有名管道可以用于不相关的进程间通信。

有名管道的高级应用:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h> //FIFO创建需要的头文件
#include <fcntl.h>    //open函数
#include <unistd.h>   //延时函数sleep、read、write
#include <string.h>   //strlen函数
#include <sys/wait.h>
int main(int argc, char const *argv[])
{
    // 在当前路径下创建两个有名管道文件,设置用户、用户组和其他用户权限为可读可写
    mkfifo("fifo_boy", 0666);
    mkfifo("fifo_girl", 0666);

    // 创建子进程
    int i = 0;
    for (i = 0; i < 2; i++)
    {
        pid_t pid = fork();
        if (pid == 0)
        {
            break; // 防止子进程创建子进程,因为我们只需要两个子进程
        }
    }

    if (i == 0) // 表示年龄最小的子进程
    {
#ifdef GIRL
        // 以读方式打开fifo_girl文件
        int fd = open("fifo_girl", O_RDONLY);
#endif
#ifdef BOY
        int fd = open("fifo_boy", O_RDONLY);
#endif

        if (fd < 0)
        {
            perror("open:");
            return 0;
        }

        // 一号子进程一直接受数据
        while (1)
        {
            char buf[128] = "";
            read(fd, buf, sizeof(buf));

#ifdef GIRL
            printf("收到girl的信息:%s\n", buf);
#endif
#ifdef BOY
            printf("收到boy的信息:%s\n", buf);
#endif

            if (strcmp(buf, "good bye") == 0)
                break;
        }

        close(fd);
        _exit(-1);
    }
    else if (i == 1) // 表示二号子进程
    {
#ifdef GIRL
        // 以写方式打开fifo_girl文件
        int fd = open("fifo_boy", O_WRONLY);
#endif
#ifdef BOY
        int fd = open("fifo_girl", O_WRONLY);
#endif

        if (fd < 0)
        {
            perror("open:");
            return 0;
        }

        // 二号子进程一直发送数据
        while (1)
        {
            // 从键盘获取输入
            char buf[128] = "";
            fgets(buf, sizeof(buf), stdin);
            buf[strlen(buf) - 1] = '\0';

            // 发送数据
            write(fd, buf, strlen(buf));

            if (strcmp(buf, "good bye") == 0)
                break;
        }
        sleep(1);
        close(fd);
        _exit(-1);
    }
    else if (i == 2) // 父进程,用来回收子进程资源
    {
        while (1)
        {
            pid_t pid = waitpid(-1, NULL, WNOHANG); // 等待任一子进程退出,不关心子进程的退出状态,
            if (pid > 0)
            {
                printf("子进程%d退出了\n", pid);
            }
            else if (pid == 0)
                continue;
            else if (pid == -1)
            {
                printf("所有的子进程都退出了\n");
                break;
            }
        }
    }
    return 0;
}

输出总结果:

 运行子进程:./boy和运行二号子进程:./girl,向负责发送数据的二号子进程发送数据

输出结果:

向负责接收数据的一号子进程发送数据

输出结果:

 实际上这是利用了有名管道的特点,发送数据的时候,是往girl的二号子进程发送数据,位置调换

#ifdef GIRL
        // 以写方式打开fifo_girl文件
        int fd = open("fifo_boy", O_WRONLY);
#endif
#ifdef BOY
        int fd = open("fifo_girl", O_WRONLY);
#endif

介绍:创建了两个有名管道文件,两个子进程和一个父进程,父进程用来回收子进程的资源,一号子进程用来不停地接受数据,二号子进程用来不停地发送数据。

三、消息队列

前面讲到管道和有名管道,管道使用pipe函数来创建,有名管道使用mkfifo函数来创建,管道用于父子进程间的通信,因为管道(pipe)是基于文件描述符的,子进程继承了父进程的文件描述符。

有名管道(fifo)可以用于不相关的进程间的通信,因为有名管道是基于有名管道文件的,通过有名管道名来读写通信。

而消息队列则可以用于多对多的进程间的通信。

消息队列其实是消息的链表,数据也是存放在内存中,由内核维护。

消息队列的特点:

1、消息队列的消息有数据类型和格式的;

2、消息队列可以实现消息的随机查询,因为是消息的链表,,所以消息可以按消息的类型读取(同类型的数据先进先出),也可以先进先出的次序读取;

3、消息队列允许一个或多个进程向他写入或读取消息;(因为存放在内存中,所以读取后数据也会消失)

4、每一个消息队列都有消息队列标识符,消息队列的标识符在整个系统中是唯一的,用于区分不同的消息队列,不同的进程通过消息队列标识符来打开消息队列(消息队列标识符是一个非零整数)

5、消息队列不仅具有消息队列标识符,还具有整个系统唯一的IPC键值,IPC键值是由我们定义的

IPC键值作用是进程通过IPC键值可以访问IPC资源,实现进程间的资源共享。

创建消息队列的步骤:

1、先获取一个唯一的消息队列的IPC键值

#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
功能:
    获得项目相关的唯一的 IPC 键值。
参数:
    pathname:路径名
    proj_id:项目 ID,非 0 整数(只有低 8 位有效)
返回值:
    成功返回 key 值,失败返回 -1。

2、创建消息队列,成功返回唯一的消息队列标识符

#include <sys/msg.h>
int msgget(key_t key, int msgflg);
功能:
    创建一个新的或打开一个已经存在的消息队列。不同的进程调用此函数,
只要用相同的 key 值就能得到同一个消息队列的标识符。
参数:
    key:IPC 键值。
    msgflg:标识函数的行为及消息队列的权限。
参数:
    msgflg 的取值:
        IPC_CREAT:创建消息队列。
        IPC_EXCL:检测消息队列是否存在。
        位或权限位:消息队列位或权限位后可以设置消息队列的访问权限,格式和 open 函数的
mode_t 一样,但可执行权限未使用。
返回值:
    成功:消息队列的标识符,失败:返回-1

案例:

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int main(int argc, char const *argv[])
{

    key_t key = ftok("/", 2023); // 8位有效
    printf("key=%#x\n", key);
    printf("key=%d\n", key);

    int msg_id = msgget(key, IPC_CREAT | 0666);
    printf("msg_id=%d\n", msg_id);


     key_t key1 = ftok("/", 2024); // 8位有效
    printf("key1=%#x\n", key1);
    printf("key1=%d\n", key1);

    int msg_id1 = msgget(key1, IPC_CREAT | 0666);
    printf("msg_id1=%d\n", msg_id1);
    return 0;
}

输出结果:

 

 消息队列创建之后,可以通过命令来查看消息队列的状态和手动删除消息队列

终端查看消息队列:ipcs -q

终端删除消息队列:ipcrm -q msg_id

3、消息队列创建之后,就是发送消息

#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp,size_t msgsz, int msgflg);
功能:
    将新消息添加到消息队列。
参数:
    msqid:消息队列的标识符。
    msgp:待发送消息结构体的地址。
    msgsz:消息正文的字节数。
    msgflg:函数的控制属性
        0:msgsnd 调用阻塞直到条件满足为止。
        IPC_NOWAIT: 若消息没有立即发送则调用该函数的进程会立即返回。
返回值:
    成功:0;失败:返回-1。

3.1、消息队列是有格式的,消息队列的格式是一种结构体

typedef struct _msg
{
    long mtype; /*消息类型  接受者感兴趣的类型*/
    char mtext[100]; /*消息正文*/
    ... /*消息的正文可以有多个成员*/
}MSG;

4、接收消息

#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,
int msgflg);
功能:
    从标识符为 msqid 的消息队列中接收一个消息。一旦接收消息成功,则消息
在消息队列中被删除。
参数:
    msqid:消息队列的标识符,代表要从哪个消息列中获取消息。
    msgp: 存放消息结构体的地址。
    msgsz:消息正文的字节数。
    msgtyp:消息的类型、可以有以下几种类型
        msgtyp = 0:返回队列中的第一个消息
        msgtyp > 0:返回队列中消息类型为 msgtyp 的消息
        msgtyp < 0:返回队列中消息类型值小于或等于 msgtyp 绝对值的消息,如果这
种消息有若干个,则取类型值 最小的消息
    msgflg:函数的控制属性
        0:msgrcv 调用阻塞直到接收消息成功为止。
        MSG_NOERROR:若返回的消息字节数比 nbytes 字节数多,则消息就会截短到 nbytes 字节, 且不通知消息发送进程。
        IPC_NOWAIT:调用进程会立即返回。若没有收到消息则立即返回-1。
返回值:
    成功返回读取消息的长度,失败返回-1

5、消息队列的控制

#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
功能:
    对消息队列进行各种控制,如修改消息队列的属性,或删除消息消息队列。
参数:
    msqid:消息队列的标识符。
    cmd:函数功能的控制。
    buf:msqid_ds 数据类型的地址,用来存放或更改消息队列的属性。
cmd:函数功能的控制
    IPC_RMID:删除由 msqid 指示的消息队列,将它从系统中删除并破坏相关
数据结构。
    IPC_STAT:将 msqid 相关的数据结构中各个元素的当前值存入到由 buf
指向的结构中。
    IPC_SET:将 msqid 相关的数据结构中的元素设置为由 buf 指向的结构中
的对应值。
返回值:
    成功:返回 0;失败:返回 -1

消息队列实现多进程通信案例:

创建三个.c文件,代码都是重复的,也可以根据前面所写的宏函数来实现一个文件多个进程。

每一个可执行文件都创建了三个进程(父子进程),不同的进程间都可以相互通信,即多对多的进程间数据通信。

分开存储的代码实现:

girl.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

enum
{
    boy = 1,
    girl = 2,
    god = 3
};

char *get_name[] = {"boy", "girl", "god"};
int name_num[] = {1, 2, 3};
int n = sizeof(get_name) / sizeof(get_name[0]);

// 消息队列的消息结构体
typedef struct
{
    long mtype;    // 消息的类型
    char name[18]; // 消息正文
    char text[128];

} MSG;

int main(int argc, char const *argv[])
{
    // 1、先获取唯一的IPC键值
    key_t key = ftok("/", 2023); // 在根目录下,项目ID为2023,返回值为IPC键值
    printf("%d\n", key);

    // 2、创建消息队列并获取唯一的消息队列标识符,消息队列中的消息是有格式的,是一种结构体
    int msg_id = msgget(key, IPC_CREAT | 0666);
    printf("%d\n", msg_id);

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

    if (i == 0) // 一号子进程接收消息
    {
        while (1)
        {
            MSG msg;
            msgrcv(msg_id, &msg, sizeof(MSG) - sizeof(long), girl, 0);
            printf("\r%s发来的消息: %s\n", msg.name, msg.text);
            printf("请输入接收者的姓名:");
            fflush(stdout); // 因为printf没有\n,需要强制刷新
        }
    }
    else if (i == 1) // 二号子进程发送消息
    {
        while (1)
        {
            printf("请输入接收者姓名(给指定的人发送消息):");
            char name[18] = "";
            fgets(name, sizeof(name), stdin);
            name[strlen(name) - 1] = '\0';

            int i = 0;
            for (i = 0; i < n; i++)
            {
                if (strcmp(get_name[i], name) == 0)
                    break;
            }
            if (i == n)
            {
                printf("输入的用户名不存在\n");
                continue;
            }
            printf("请输入发送的消息:");
            char buf[128] = "";
            fgets(buf, sizeof(buf), stdin);
            buf[strlen(buf) - 1] = '\0';

            MSG msg;
            msg.mtype = name_num[i];  // 消息类型
            strcpy(msg.name, "girl"); // 发送者姓名
            strcpy(msg.text, buf);    // 发送者发送的信息

            // 发送消息
            msgsnd(msg_id, &msg, sizeof(MSG) - sizeof(long), 0);
        }
    }
    else if (i == 2) // 父进程,用来回收子进程的资源
    {
        while (1)
        {
            pid_t pid = waitpid(-1, NULL, WNOHANG);
            if (pid > 0)
            {
                printf("子进程%d退出了\n", pid);
            }
            else if (pid == 0)
                continue;
            else if (pid == -1)
            {
                printf("所有的子进程都退出了\n");
                break;
            }
        }
    }

    return 0;
}

boy.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

enum
{
    boy = 1,
    girl = 2,
    god = 3
};

char *get_name[] = {"boy", "girl", "god"};
int name_num[] = {1, 2, 3};
int n = sizeof(get_name) / sizeof(get_name[0]);

// 消息队列的消息结构体
typedef struct
{
    long mtype;    // 消息的类型
    char name[18]; // 消息正文
    char text[128];

} MSG;

int main(int argc, char const *argv[])
{
    // 1、先获取唯一的IPC键值
    key_t key = ftok("/", 2023); // 在根目录下,项目ID为2023,返回值为IPC键值
    printf("%d\n", key);

    // 2、创建消息队列并获取唯一的消息队列标识符,消息队列中的消息是有格式的,是一种结构体
    int msg_id = msgget(key, IPC_CREAT | 0666);
    printf("%d\n", msg_id);

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

    if (i == 0) // 一号子进程接收消息
    {
        while (1)
        {
            MSG msg;
            msgrcv(msg_id, &msg, sizeof(MSG) - sizeof(long), boy, 0);
            printf("\r%s发来的消息: %s\n", msg.name, msg.text);
            printf("请输入接收者的姓名:");
            fflush(stdout); // 因为printf没有\n,需要强制刷新
        }
    }
    else if (i == 1) // 二号子进程发送消息
    {
        while (1)
        {
            printf("请输入接收者姓名(给指定的人发送消息):");
            char name[18] = "";
            fgets(name, sizeof(name), stdin);
            name[strlen(name) - 1] = '\0';

            int i = 0;
            for (i = 0; i < n; i++)
            {
                if (strcmp(get_name[i], name) == 0)
                    break;
            }
            if (i == n)
            {
                printf("输入的用户名不存在\n");
                continue;
            }
            printf("请输入发送的消息:");
            char buf[128] = "";
            fgets(buf, sizeof(buf), stdin);
            buf[strlen(buf) - 1] = '\0';

            MSG msg;
            msg.mtype = name_num[i]; // 消息类型
            strcpy(msg.name, "boy"); // 发送者姓名
            strcpy(msg.text, buf);   // 发送者发送的信息

            // 发送消息
            msgsnd(msg_id, &msg, sizeof(MSG) - sizeof(long), 0);
        }
    }
    else if (i == 2) // 父进程,用来回收子进程的资源
    {
        while (1)
        {
            pid_t pid = waitpid(-1, NULL, WNOHANG);
            if (pid > 0)
            {
                printf("子进程%d退出了\n", pid);
            }
            else if (pid == 0)
                continue;
            else if (pid == -1)
            {
                printf("所有的子进程都退出了\n");
                break;
            }
        }
    }

    return 0;
}

god.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

enum
{
    boy = 1,
    girl = 2,
    god = 3
};

char *get_name[] = {"boy", "girl", "god"};
int name_num[] = {1, 2, 3};
int n = sizeof(get_name) / sizeof(get_name[0]);

// 消息队列的消息结构体
typedef struct
{
    long mtype;    // 消息的类型
    char name[18]; // 消息正文
    char text[128];

} MSG;

int main(int argc, char const *argv[])
{
    // 1、先获取唯一的IPC键值
    key_t key = ftok("/", 2023); // 在根目录下,项目ID为2023,返回值为IPC键值
    printf("%d\n", key);

    // 2、创建消息队列并获取唯一的消息队列标识符,消息队列中的消息是有格式的,是一种结构体
    int msg_id = msgget(key, IPC_CREAT | 0666);
    printf("%d\n", msg_id);

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

    if (i == 0) // 一号子进程接收消息
    {
        while (1)
        {
            MSG msg;
            msgrcv(msg_id, &msg, sizeof(MSG) - sizeof(long), god, 0);
            printf("\r%s发来的消息: %s\n", msg.name, msg.text);
            printf("请输入接收者的姓名:");
            fflush(stdout); // 因为printf没有\n,需要强制刷新
        }
    }
    else if (i == 1) // 二号子进程发送消息
    {
        while (1)
        {
            printf("请输入接收者姓名(给指定的人发送消息):");
            char name[18] = "";
            fgets(name, sizeof(name), stdin);
            name[strlen(name) - 1] = '\0';

            int i = 0;
            for (i = 0; i < n; i++)
            {
                if (strcmp(get_name[i], name) == 0)
                    break;
            }
            if (i == n)
            {
                printf("输入的用户名不存在\n");
                continue;
            }
            printf("请输入发送的消息:");
            char buf[128] = "";
            fgets(buf, sizeof(buf), stdin);
            buf[strlen(buf) - 1] = '\0';

            MSG msg;
            msg.mtype = name_num[i];  // 消息类型
            strcpy(msg.name, "god"); // 发送者姓名
            strcpy(msg.text, buf);    // 发送者发送的信息

            // 发送消息
            msgsnd(msg_id, &msg, sizeof(MSG) - sizeof(long), 0);
        }
    }
    else if (i == 2) // 父进程,用来回收子进程的资源
    {
        while (1)
        {
            pid_t pid = waitpid(-1, NULL, WNOHANG);
            if (pid > 0)
            {
                printf("子进程%d退出了\n", pid);
            }
            else if (pid == 0)
                continue;
            else if (pid == -1)
            {
                printf("所有的子进程都退出了\n");
                break;
            }
        }
    }

    return 0;
}

编译:

输出结果一:

boy发送消息给girl

girl收到boy发来的消息

 输出结果二:

god给boy发送消息:

 boy收到god消息:

 输出结果三:

boy收到来信后回复god消息:

god收到boy消息:

 

 输出结果四:

girl给boy回送消息:

 boy收到girl消息:

如果消息未读取,会一直存在内存中,除非手动重启或删除,证明运行之后可看到消息被读取:

 

运行三个可执行文件的时候可以看到他们的IPC键值都是整数-419364862,消息队列标识符都是0。

通过命令查看终端的消息队列:ipcs -q

下图中的IPC键值变化,因为重启了内核,重新编译可执行文件的结果

 共创建了九个进程,三个不相关进程用于通信,每个可执行文件实例化的三个父子进程一号进程用来接收消息,二号进程用来发送消息,父进程用来回收子进程的资源。

 使用宏将三个可执行文件集合在一起,降低了代码的重复率:

使用宏:

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

// 使用宏将三个代码文件简化为一个文件,降低了代码的重复率,
// 比较三个文件将不同找出来,用宏替换
enum
{
    boy = 1,
    girl = 2,
    god = 3
};

char *get_name[] = {"boy", "girl", "god"};
int name_num[] = {1, 2, 3};
int n = sizeof(get_name) / sizeof(get_name[0]);

// 消息队列的消息结构体
typedef struct
{
    long mtype;    // 消息的类型
    char name[18]; // 消息正文
    char text[128];

} MSG; // 定义了结构体的类型为MSG

int main(int argc, char const *argv[])
{
    // 1、先获取唯一的IPC键值
    key_t key = ftok("/", 2023); // 在根目录下,项目ID为2023,返回值为IPC键值
    printf("%d\n", key);

    // 2、创建消息队列并获取唯一的消息队列标识符,消息队列中的消息是有格式的,是一种结构体
    int msg_id = msgget(key, IPC_CREAT | 0666);
    printf("%d\n", msg_id);

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

    if (i == 0) // 一号子进程接收消息
    {
        while (1)
        {
            MSG msg;                                                   // 定义了结构体的变量
            msgrcv(msg_id, &msg, sizeof(MSG) - sizeof(long), RECV, 0); // msgrcv函数的第四个参数是消息类型long mtype,用RECV替代,在预处理阶段的时候给RECV赋值
            printf("\r%s发来的消息: %s\n", msg.name, msg.text);
            printf("请输入接收者的姓名:");
            fflush(stdout); // 因为printf没有\n,需要强制刷新
        }
    }
    else if (i == 1) // 二号子进程发送消息
    {
        while (1)
        {
            printf("请输入接收者姓名(给指定的人发送消息):");
            char name[18] = "";
            fgets(name, sizeof(name), stdin);
            name[strlen(name) - 1] = '\0';

            int i = 0;
            for (i = 0; i < n; i++)
            {
                if (strcmp(get_name[i], name) == 0)
                    break;
            }
            if (i == n)
            {
                printf("输入的用户名不存在\n");
                continue;
            }
            printf("请输入发送的消息:");
            char buf[128] = "";
            fgets(buf, sizeof(buf), stdin);
            buf[strlen(buf) - 1] = '\0';

            MSG msg;
            msg.mtype = name_num[i]; // 消息类型

            for (i = 0; i < n; i++) // 使用前面的for循环来得到名字
            {
                if (RECV == name_num[i])
                    break;
            }
            strcpy(msg.name, get_name[i]); // 发送者姓名
            strcpy(msg.text, buf);   // 发送者发送的信息

            // 发送消息
            msgsnd(msg_id, &msg, sizeof(MSG) - sizeof(long), 0);
        }
    }
    else if (i == 2) // 父进程,用来回收子进程的资源
    {
        while (1)
        {
            pid_t pid = waitpid(-1, NULL, WNOHANG);
            if (pid > 0)
            {
                printf("子进程%d退出了\n", pid);
            }
            else if (pid == 0)
                continue;
            else if (pid == -1)
            {
                printf("所有的子进程都退出了\n");
                break;
            }
        }
    }

    return 0;
}

 编译和执行结果:

 

输出结果一:

boy给girl发送消息,god给boy发送消息

 输出结果二:

girl给boy发送消息,god给girl发送消息

 看到输出结果是不是感到神奇呢?动手试试吧。

最后通过命令删除消息队列:

sudo ipcrm -q 0//删除消息队列,注意进程要先结束

sudo ipcs -q//查看消息队列

进程间的通信方式还可以使用磁盘映射,

四、磁盘映射

磁盘映射(mmap)的原理就是将一个磁盘文件映射到内存中的一个缓冲区中。(相互映射)

磁盘映射的特点:因为进程只存在于内存中,当进程在缓冲区中写入数据,就会将数据写入到文件,同理,当进程从缓冲区读取数据,相当于从磁盘文件上读取数据。其可以不使用write和read函数来写入和读取数据(I/O操作),可以通过指针来完成I/O操作。且有别于上面三种通信方式,磁盘映射的数据存储在文件内,可以重复读取。

磁盘映射的步骤:

1、得到要操作的文件的文件描述符,并扩展文件的大小

int truncate(const char *path, off_t length);
path 要拓展的文件
length 要拓展的长度

2、建立磁盘和内存的映射

void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
参数:
    addr 地址,填 NULL
    length 长度 要申请的映射区的长度
    prot 权限
        PROT_READ 可读
        PROT_WRITE 可写
flags 标志位
    MAP_SHARED 共享的 -- 对映射区的修改会影响源文件
    MAP_PRIVATE 私有的
fd 文件描述符 需要打开一个文件
offset 指定一个偏移位置 ,从该位置开始映射
返回值
    成功 返回映射区的首地址
    失败 返回 MAP_FAILED 也就是:(void *) -1

3、释放磁盘映射的映射区(缓冲区)

int munmap(void *addr, size_t length);
    addr 映射区的首地址
    length 映射区的长度
返回值
    成功 返回 0
    失败 返回 -1

磁盘映射的案例:

创建了两个文件,一个用来读取缓冲区内容,一个向缓冲区写入数据

mmap_write.c

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

int main(int argc, char const *argv[])
{
    //1、先得到要操作的文件的文件描述符
    int fd=open("tmp",O_RDWR|O_CREAT,0666);//在当前目录下以可读可写的方式打开文件,如果不存在就创建,设置文件的权限为用户、组用户和其他用户权限为可读可写

    if(fd<0)
    {
        perror("open:");
        return 0;
    }

    //2、扩展文件大小
    truncate("tmp",128);

    //3、建立磁盘映射
    char *p=(char *)mmap(NULL,128,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);

    //4、写入数据
    char text[128]="";
    fgets(text,sizeof(text),stdin);
    strcpy(p,text);

    //释放缓冲区
    munmap(p,128);

    return 0;
}

 mmap_read.c

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

int main(int argc, char const *argv[])
{
    // 1、先得到要操作的文件的文件描述符
    int fd = open("tmp", O_RDWR | O_CREAT, 0666); // 在当前目录下以可读可写的方式打开文件,如果不存在就创建,设置文件的权限为用户、组用户和其他用户权限为可读可写

    if (fd < 0)
    {
        perror("open:");
        return 0;
    }

    // 2、扩展文件大小
    truncate("tmp", 128);

    // 3、建立磁盘映射
    char *p = (char *)mmap(NULL, 128, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

    // 4、读取数据
    printf("%s\n", p);

    // 释放缓冲区
    munmap(p, 128);

    return 0;
}

输出结果:

五、共享内存

共享内存和磁盘映射异曲同工,他们之间有着相似之处

共享内存是在内存中划定一块内存区间,供多进程间访问

共享内存的特点:

读取速度快

注意点:当某个进程向共享内存区域写入数据,别的进程不要读取或写入数据,因此需要设置权限(互斥)。

共享内存的创建:

1、先获取一个IPC键值

#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
功能:
    获得项目相关的唯一的 IPC 键值。
参数:
    pathname:路径名
    proj_id:项目 ID,非 0 整数(只有低 8 位有效)
返回值:
    成功返回 key 值,失败返回 -1。

2、创建共享内存,获取一个共享内存标识符

#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size,int shmflg);
    功能:创建或打开一块共享内存区
参数:
    key:IPC 键值
    size:该共享存储段的长度(字节)
    shmflg:标识函数的行为及共享内存的权限。
参数:
shmflg:
    IPC_CREAT:如果不存在就创建
    IPC_EXCL:如果已经存在则返回失败
    位或权限位:共享内存位或权限位后可以设置共享内存的访问权限,格式
和 open 函数的 mode_t 一样,但可执行权限未使用。
返回值:
    成功:返回共享内存标识符。
    失败:返回-1。

3、共享内存区间映射,返回共享内存段映射地址(连接共享内存)

#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr,
int shmflg);
功能:
    将一个共享内存段映射到调用进程的数据段中。
参数:
    shmid:共享内存标识符。
    shmaddr:共享内存映射地址(若为 NULL 则由系 统自动指
定),推荐使用 NULL。
    shmflg:共享内存段的访问权限和映射条件
        0:共享内存具有可读可写权限。
        SHM_RDONLY:只读。
        SHM_RND:(shmaddr 非空时才有效)
没有指定 SHM_RND 则此段连接到 shmaddr 所指定的地址上(shmaddr 必需
页对齐)。
        指定了 SHM_RND 则此段连接到 shmaddr- shmaddr%SHMLBA 所表示的地址
上。
返回值:
    成功:返回共享内存段映射地址
    失败:返回 -1

4、共享内存区控制

#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd,
struct shmid_ds *buf);
功能:共享内存空间的控制。
参数:
    shmid:共享内存标识符。
    cmd:函数功能的控制。
    buf:shmid_ds 数据类型的地址,用来存放或修改共享内存的属性。
cmd:函数功能的控制
    IPC_RMID:删除。
    IPC_SET:设置 shmid_ds 参数。
    IPC_STAT:保存 shmid_ds 参数。
    SHM_LOCK:锁定共享内存段(超级用户)。
    SHM_UNLOCK:解锁共享内存段。
返回值:
    成功返回 0,失败返回 -1。

5、解除共享内存映射区

#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
功能:
    将共享内存和当前进程分离(仅仅是断开联系并不删除共享内存)。
参数:
    shmaddr:共享内存映射地址。
返回值:
    成功返回 0,失败返回 -1。

共享内存创建进程间的通信案例:

创建两个.c文件

 shmid_write.c

#include<stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>//ftok函数,获取IPC键值(根据你输入的项目ID)
#include <sys/shm.h>//shmget、shmat、shmctl、shmdt四个函数,功能分别是得到共享内存标识符,与共享内存建立连接、控制共享内存、解除共享内存映射
#include<string.h>//strcpy函数

int main(int argc, char const *argv[])
{
    //1、先获取唯一的IPC键值
    key_t key=ftok("/",2023);

    //2、得到共享内存标识符shmid
    int shmid=shmget(key,128,IPC_CREAT|0666);//共享内存这里不要加IPC_EXEL,因为共享内存系统一般都会创建的

    //3、连接共享内存段
    char *p=(char *)shmat(shmid,NULL,0);//第二个参数为NULL,表示地址由系统指定,第三个参数为0表示共享内存段可读可写
    char text[128]="";
    fgets(text,sizeof(text),stdin);
    strcpy(p,text);

    //4、解除映射
    shmdt(p); // 如果这里不解除映射,往设置好的共享内存段写入数据,要确保数据在设置的大小范围内,否则会访问越界,引发数据泄露或损坏等

    return 0;
}


shmid_read.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h> //ftok函数,获取IPC键值(根据你输入的项目ID)
#include <sys/shm.h> //shmget、shmat、shmctl、shmdt四个函数,功能分别是得到共享内存标识符,与共享内存建立连接、控制共享内存、解除共享内存映射

int main(int argc, char const *argv[])
{
    // 1、先获取唯一的IPC键值
    key_t key = ftok("/", 2023);
    printf("IPC键值:%#x\n", key);

    // 2、得到共享内存标识符shmid
    int shmid = shmget(key, 128, IPC_CREAT | 0666);
    printf("共享内存段标识符:%d\n", shmid);
    // 如果IPC_EXCL可能出现多个,可以做一个检测比如
    /*if(shmid==-1)
    {
        perror("shmid:");
        return 0;
    }*/

    // 3、连接共享内存段
    char *p = (char *)shmat(shmid, NULL, 0); // 第二个参数为NULL,表示地址由系统指定,第三个参数为0表示共享内存段可读可写
    printf("%s\n", p);

    // 4、解除映射
    shmdt(p);

    return 0;
}

 编译:

 输出结果:

加入printf()函数打印出IPC键值和shmid

 

 使用shell命令查看共享内存段状态

ipcs -m

根据上面输出的结果得到创建的共享内存段在红色框架内,IPC键值为十六进制:0xe7010002,shmid(共享内存标识符):1736716,拥有者root,共享内存段权限是0666,可存储字节数:128,连接数(有几个进程使用了该共享内存段):0(因为代码执行完毕,shmdt函数就解除了进程与共享内存段的映射),状态为空(表示无目标)。

 使用命令:

//shmid是实际的共享内存标识符

ipcrm -m shmid

//如果是普通用户加sudo

sudo ipcrm -m shmid

 

ipcs -m查看,可以看到共享内存段已经没有了。(注意如果进程没有结束,此时销毁共享内存段,系统会提供一个临时的共享内存段,其IPC键值为0x00000000,当进程结束会被系统内核回收)

 六、套接字

以上五个进程间的基本通信方式只能用于同一个主机的同一个系统内的父子进程或多进程间的通信。套接字(socket)的出现解决了不同主机的进程间的通信。

socket描述的是所有网络编程接口的统称,他是特殊的文件描述符,表示通信一端的端点。

和前面五个基本通信方式一样,建立通信需要使用专用的函数

创建套接字的函数:socket

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

int socket(int family,int type,int protocol);
功能:
    创建一个用于网络通信的 socket 套接字(描述符)
参数:
    family:协议族(AF_INET、AF_INET6、PF_PACKET 等)
    type:套接字类(SOCK_STREAM、SOCK_DGRAM、SOCK_RAW 等)
    protocol:协议类别(0、IPPROTO_TCP、IPPROTO_UDP 等
返回值:
    套接字
特点:
    创建套接字时,系统不会分配端口
    创建的套接字默认属性是主动的,即主动发起服务的请求;当作为服务器时,往往需要修改为被动的


AF_INET表示IPv4协议,该协议是应用框架层协议,
AF_INET6表示IPv6协议,该协议也是应用框架层协议
PF_PACKET表示上面两个协议都兼有

//SOCK_STREAM表示获取可靠的流式传输数据,用于TCP协议
//SOCK_DGRAM表示获取不可靠的数据报传输数据,用于UDP协议
//SOCK_RAW表示原始套接字,直接从数据链路层获取数据

因为socket编程涉及到不同网络,因此还需要引入其他内容

字节序:是计算机存储数据的顺序

字节序作用:确保不同存储方式的计算机之间数据通信的正确性。

一般在异构计算机或小端存储方式的计算机向网络发送数据或网络向小端存储的计算机发送数据的时候会涉及到字节序的转换问题。(如果计算机和网络一样,都是大端存储就不需要用到字节序转换函数)

字节序转换函数:htonl和htons,ntohl和ntohs

htons函数是主机向网络发送数据,s是无符号短整型(uint16_t),表示2个字节(16位)。


头文件:
    #include <arpa/inet.h>
uint16_t htons(uint16_t hostint16);
功能:
    将 16 位主机字节序数据转换成网络字节序数据
参数:
    uint16_t:unsigned short int
    hostint16:待转换的 16 位主机字节序数据
返回值:
    成功:返回网络字节序的值

htonl函数是主机向网络发送数据,l是无符号整型(uint32_t),表示4个字节(32位)。

头文件:
    #include <arpa/inet.h>
uint32_t htonl(uint32_t hostint32);
功能:
    将 32 位主机字节序数据转换成网络字节序数据
参数:
    hostint32:待转换的 32 位主机字节序数据
返回值:
    成功:返回网络字节序的值

 ntohs函数是网络向主机发送数据,s是无符号短整型(uint16_t),表示2个字节(16位)。

头文件:
    #include <arpa/inet.h
uint16_t ntohs(uint16_t netint16);
功能:
    将 16 位网络字节序数据转换成主机字节序数据
参数:
    uint16_t: unsigned short int
    netint16:待转换的 16 位网络字节序数据
返回值:
    成功:返回主机字节序的值

 ntohl函数是网络向主机发送数据,l是无符号整型(uint32_t),表示4个字节(32位)。

头文件:
    #include <arpa/inet.h>
uint32_t ntohl(uint32_t netint32);
功能:
    将 32 位网络字节序数据转换成主机字节序数据
参数:
    uint32_t: unsigned int
    netint32:待转换的 32 位网络字节序数据
返回值:
    成功:返回主机字节序的值

测试Ubuntu系统是否是小端存储,因为我们知道网络只有大端存储,所以如果数据颠倒了,说明Ubuntu系统的数据存储的顺序和网络不同。

测试案例:

#include<stdio.h>
#include<arpa/inet.h>

int main(int argc, char const *argv[])
{
    //模拟发送,测试Ubuntu系统的数据存储方式是大端存储还是小端存储方式
    unsigned short data1=0x0102;
    unsigned long data2=0x01020304;

    printf("htons(data1)=%#x\n",htons(data1));
    printf("htonl(data2)=%#x\n",htonl(data2));

    return 0;
}

输出结果:

 通过数据和输出结果比较,数据颠倒了,说明Ubuntu系统的字节序是小端存储方式。

如果想测试系统的存储方式可以通过这个来测试。(注意需要搭建好环境、库函数)

除了字节序之外,不同的网络之间还涉及到协议和端口、IP地址转换问题

IP地址的转换:

我们所识别到的IP地址是点分十进制的字符串,如192.1681.0,而计算机只能识别二进制数

因此需要进行转换,(这里以IPv4为例)

IP地址转换函数:

inet_pton函数:将点分十进制的IP地址转换为无符号整型

int inet_pton(int family,const char *strptr, void *addrptr)
功能:
    将点分十进制数串转换成 32 位无符号整数
参数:
    family 协议族  AF_INET
    strptr 点分十进制数串的首元素地址
    addrptr 32 位无符号整数的地址(已经是大端格式)
返回值:
    成功返回 1 、 失败返回其它
头文件:
    #include <arpa/inet.h>

 inet_ntop函数:将无符号整型转换为点分十进制的IP地址

整型数据转字符串格式 ip 地址
const char *inet_ntop(int family, const void *addrptr,
char *strptr, size_t len)
功能:
    将 32 位无符号整数转换成点分十进制数串
参数:
    family 协议族 AF_INET
    addrptr 32 位无符号整数
    strptr 点分十进制数串
    len strptr 缓存区长度
len 的宏定义
    #define INET_ADDRSTRLEN 16 //for ipv4
    #define INET6_ADDRSTRLEN 46 //for ipv6
返回值:
    成功:则返回字符串的首地址
    失败:返回 NULL
头文件:
    #include <arpa/inet.h>

IP地址转换以192.168.1.1为例

案例:

#include <stdio.h>
#include <arpa/inet.h>
int main(int argc, char const *argv[])
{
    // 点分十进制数串
    char ip_str[] = "192.168.1.1";

    // 32位无符号整数
    unsigned int ip_data = 0;

    // 将点分十进制数串 转换成 32位无符号整数
    inet_pton(AF_INET, ip_str, &ip_data);

    printf("IP地址:%u\n", ip_data);

    // 将32位无符号整型转换为点分十进制的字符串
    char ip_addr[16] = ""; // 这里重新定义,防止是因为上面的定义导致紊乱
    inet_ntop(AF_INET, &ip_data, ip_addr, 16);
    printf("IP地址:%s\n", ip_addr);
    return 0;
}

输出结果:

 除了字节序和IP地址转换、还有协议

协议有很多,其中IP协议是网络通信最基本的协议

IP协议分为IPv4协议和IPv6协议

这里以IPv4协议为例:

IPv4地址的结构体:

头文件:#include <netinet/in.h>
//IPv4地址结构体
struct sockaddr_in
{
    sa_family_t sin_family;//2 字节  AF_INET--IPv4   AF_INET6---IPV6   协议类别
    in_port_t sin_port;//2 字节   端口   
    struct in_addr sin_addr;//4 字节  IP地址
    char sin_zero[8]//8 字节
};
struct in_addr
{
    in_addr_t s_addr;//4 字节
};



//通用地址结构体 struct sockaddr
struct sockaddr
{
    sa_family_t sa_family; // 2 字节
    char sa_data[14] //14 字节
};

//在定义源地址和目的地址结构的时候,选用 struct sockaddr_in;
//当调用编程接口函数,且该函数需要传入地址结构时需要用通用结构体 struct sockaddr

 通信需要发送和接受函数

sendto函数发送消息

ssize_t sendto
(
    int sockfd,                 //套接字
    const void *buf,             //发送数据的缓冲区
    size_t nbytes,              //发送数据的缓冲区的大小
    int flags,                   //套接字标识符,一般为 0
     const struct sockaddr *to,  //指向目的主机地址结构体的指针
    socklen_t addrlen            //指针to所指向内容的长度
)

返回值:

成功:发送数据的字符数
失败: -1

 recvfrom函数接收数据

ssize_t recvfrom(int sockfd, void *buf,
size_t nbytes,int flags,
struct sockaddr *from,
socklen_t *addrlen);
功能:
    接收 UDP 数据,并将源地址信息保存在 from 指向的结构中
参数:
    sockfd: 套接字
    buf:接收数据缓冲区
    nbytes:接收数据缓冲区的大小
    flags: 套接字标志(常为 0)
    from: 源地址结构体指针,用来保存数据的来源
    addrlen: from 所指内容的长度
注意:
    通过 from 和 addrlen 参数存放数据来源信息
    from 和 addrlen 可以为 NULL, 表示不保存数据来源
返回值:
    成功:接收到的字符数
    失败: -1

 另外接收数据一端需要绑定,将套接字和IP、端口绑定在一块,以便接收一方也能回送消息

bind函数绑定端口和IP

int bind(int sockfd,
const struct sockaddr *myaddr,socklen_t addrlen);
功能:
将本地协议地址与 sockfd 绑定
参数:
    sockfd: socket 套接字
    myaddr: 指向特定协议的地址结构指针
    addrlen:该地址结构的长度
返回值:
    成功:返回 0
    失败:其他

这里先以UDP协议为例:

通过UDP协议将数据发送出去,UDP协议面向无连接,是不可靠的传输层协议

UPD协议通信的工作流程图:

socket基于UDP协议的不同主机进程通信案例:

// socket基于UDP协议的数据通信
#include <stdio.h>
#include <sys/socket.h> //socket
#include <unistd.h>     //close
#include <string.h>     //bzero
#include <netinet/in.h> //struct sockaddr_in
#include <arpa/inet.h>  //inet_pton
#include <stdlib.h>     //atoi
#include <pthread.h>

// 发送消息的线程函数
void *send_udp_msg(void *arg)
{
    int sockfd = *(int *)arg;    // 传入的参数是指针常量,这里要强转,然后取内容
    struct sockaddr_in dst_addr; // 定义目的IP结构体
    bzero(&dst_addr, sizeof(dst_addr));
    dst_addr.sin_family = AF_INET;
    /*因为UDP协议只管发送数据,是面向无连接的不可靠协议,所以这里只需要定义协议,他的目的IP和端口不用定义*/

    while (1) // 不停发送数据
    {
        char buf[128] = "";
        fgets(buf, sizeof(buf), stdin);
        buf[strlen(buf) - 1] = 0; // fgets函数获取的没有结束符(换行符),如果不加'\0',可能不会触发刷新到终端显示

        if (strncmp(buf, "发送", 4) == 0)
        {
            char dst_ip[16] = "";
            unsigned short dst_port = 0;
            sscanf(buf, "发送 %s %hu", dst_ip, &dst_port);
            // 端口和IP地址格式转换
            dst_addr.sin_port = htons(dst_port);
            inet_pton(AF_INET, dst_ip, &dst_addr.sin_addr.s_addr);
            continue;
        }

        int len = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&dst_addr, sizeof(dst_addr));
        if (len == -1)
        {
            printf("发送失败!\n");
            return NULL;
        }

        printf("发送的数据长度:%d\n", len);
    }
}

int main(int argc, char const *argv[])
{

    // 运行的时候传端口
    // 比如执行./a.out 8000
    if (argc != 2)
    {
        printf("./a.out 8000,需要指定端口\n");
        printf("%d\n", argc);
        return 0;
    }

    // 1、创建套接字socket
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        perror("socket");
        return 0;
    }

    // 2、bind绑定,功能:将创建的套接字绑定IP和端口
    struct sockaddr_in my_addr;       // 定义一个ip地址结构体变量
    bzero(&my_addr, sizeof(my_addr)); // 结构体清零(里面可能存储了IP数据,不清零,传输的数据会累加,导致输出有误)
    my_addr.sin_family = AF_INET;
    my_addr.sin_port = htons(atoi(argv[1])); // argv[1]表示结构体中的第二个参数端口,先通过atoi将字符串转换为整形,通过htons将小端数据转成大端,目的发送到网络上
    my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    int result = bind(sockfd, (struct sockaddr *)&my_addr, sizeof(my_addr));
    if (result != 0)
    {
        perror("bind"); // 表示套接字和端口、IP绑定失败
        return 0;
    }

    // 创建发送线程
    pthread_t tid_send;                                     // ,线程是进程的执行单元,后面会详细介绍
    pthread_create(&tid_send, NULL, send_udp_msg, &sockfd); // 表示在tid_send线程,通过send_udp_msg线程函数发送消息,&socket表示传入线程函数的参数(套接字)
    pthread_detach(tid_send);                               // 线程分离函数,将线程控制权交给系统内核,好处是不用等待主线程的结束,提高资源利用率

    // 3、使用recvfrom函数接收UDP消息
    while (1)
    {
        struct sockaddr_in from_addr;           // 定义IP结构体变量,用来存储来自网络的信息
        socklen_t from_len = sizeof(from_addr); // IP结构体总大小
        unsigned char buf[1024] = "";
        int len = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&from_addr, &from_len);

        unsigned short port = 0;
        port = ntohs(from_addr.sin_port);
        char ip[16] = "";
        inet_ntop(AF_INET, &from_addr.sin_addr.s_addr, ip, 16);

        printf("获取到的ip地址和端口:%s:%hu\n", ip, port);
        printf("获取到的信息:%s\n", buf);
    }
    close(sockfd);

    return 0;
}

发送端输出结果:

输出端需要使用到另一个网络调试工具或者创建一个UDP服务端

文件太大,我上传到了代码包中。

也可以搜索netassist5.0.2或者点击下方链接:

野人家园-云想物联-嵌入式物联网技术专家NetAssist网络调试助手 V5.0.7-软件工具-野人家园

网络调试助手:

 输出端输出结果:

 接收端发送数据:

 发送端收到消息:

创建服务端和客户端案例:

创建了两个文件

 socket_udp_client.c

// socket基于UDP协议的数据通信
#include <stdio.h>
#include <sys/socket.h> //socket
#include <unistd.h>     //close
#include <string.h>     //bzero
#include <netinet/in.h> //struct sockaddr_in
#include <arpa/inet.h>  //inet_pton
#include <stdlib.h>     //atoi
#include <pthread.h>

// 发送消息的线程函数
void *send_udp_msg(void *arg)
{
    int sockfd = *(int *)arg;    // 传入的参数是指针常量,这里要强转,然后取内容
    struct sockaddr_in dst_addr; // 定义目的IP结构体
    bzero(&dst_addr, sizeof(dst_addr));
    dst_addr.sin_family = AF_INET;
    /*因为UDP协议只管发送数据,是面向无连接的不可靠协议,所以这里只需要定义协议,他的目的IP和端口不用定义*/

    while (1) // 不停发送数据
    {
        char buf[128] = "";
        fgets(buf, sizeof(buf), stdin);
        buf[strlen(buf) - 1] = 0; // fgets函数获取的没有结束符(换行符),如果不加'\0',可能不会触发刷新到终端显示

        if (strncmp(buf, "发送", 4) == 0)
        {
            char dst_ip[16] = "";
            unsigned short dst_port = 0;
            sscanf(buf, "发送 %s %hu", dst_ip, &dst_port);
            // 端口和IP地址格式转换
            dst_addr.sin_port = htons(dst_port);
            inet_pton(AF_INET, dst_ip, &dst_addr.sin_addr.s_addr);
            continue;
        }

        int len = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&dst_addr, sizeof(dst_addr));
        if (len == -1)
        {
            printf("发送失败!\n");
            return NULL;
        }

        printf("发送的数据长度:%d\n", len);
    }
}

int main(int argc, char const *argv[])
{

    // 运行的时候传端口
    // 比如执行./a.out 8000
    if (argc != 2)
    {
        printf("./a.out 8000,需要指定端口\n");
        printf("%d\n", argc);
        return 0;
    }

    // 1、创建套接字socket
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        perror("socket");
        return 0;
    }

    // 2、bind绑定,功能:将创建的套接字绑定IP和端口
    struct sockaddr_in my_addr;       // 定义一个ip地址结构体变量
    bzero(&my_addr, sizeof(my_addr)); // 结构体清零(里面可能存储了IP数据,不清零,传输的数据会累加,导致输出有误)
    my_addr.sin_family = AF_INET;
    my_addr.sin_port = htons(atoi(argv[1])); // argv[1]表示结构体中的第二个参数端口,先通过atoi将字符串转换为整形,通过htons将小端数据转成大端,目的发送到网络上
    my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    int result = bind(sockfd, (struct sockaddr *)&my_addr, sizeof(my_addr));
    if (result != 0)
    {
        perror("bind"); // 表示套接字和端口、IP绑定失败
        return 0;
    }

    // 创建发送线程
    pthread_t tid_send;                                     // ,线程是进程的执行单元,后面会详细介绍
    pthread_create(&tid_send, NULL, send_udp_msg, &sockfd); // 表示在tid_send线程,通过send_udp_msg线程函数发送消息,&socket表示传入线程函数的参数(套接字)
    pthread_detach(tid_send);                               // 线程分离函数,将线程控制权交给系统内核,好处是不用等待主线程的结束,提高资源利用率

    // 3、使用recvfrom函数接收UDP消息
    while (1)
    {
        struct sockaddr_in from_addr;           // 定义IP结构体变量,用来存储来自网络的信息
        socklen_t from_len = sizeof(from_addr); // IP结构体总大小
        unsigned char buf[1024] = "";
        int len = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&from_addr, &from_len);

        unsigned short port = 0;
        port = ntohs(from_addr.sin_port);
        char ip[16] = "";
        inet_ntop(AF_INET, &from_addr.sin_addr.s_addr, ip, 16);

        printf("获取到的ip地址和端口:%s:%hu\n", ip, port);
        printf("获取到的信息:%s\n", buf);
    }
    close(sockfd);

    return 0;
}

socket_udp_server.c

#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
int main()
{
    struct sockaddr_in dev_addr_array[16];
    memset(dev_addr_array, 0, sizeof(dev_addr_array));

    // 创建udp套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    // bind绑定固定的IP和端口
    struct sockaddr_in my_addr;
    bzero(&my_addr, sizeof(my_addr));
    my_addr.sin_family = AF_INET;
    my_addr.sin_port = htons(8009);
    my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    bind(sockfd, (struct sockaddr *)&my_addr, sizeof(my_addr));

    while (1)
    {
        unsigned char buf[1024] = "";
        struct sockaddr_in from_addr;
        socklen_t from_len = sizeof(from_addr);

        int len = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&from_addr, &from_len);
        printf("获取到的信息:%s\n", buf);
        if (strncmp(buf, "up", 2) == 0) // 收到信息
        {
            // 收到某设备信息
            int dev_num = 0;
            sscanf(buf, "up:%d", &dev_num);
            dev_addr_array[dev_num] = from_addr;
            printf("设备%d上线了\n", dev_num);
            sendto(sockfd, "ok", 2, 0, (struct sockaddr *)&from_addr, sizeof(from_addr));
        }
        else if (strncmp(buf, "data", 4) == 0) //"send:设备编号:控制数据"
        {
            int dev_num = 0;
            char data[128] = "";
            sscanf(buf, "data:%d:%s", &dev_num, data);
            // 将data数据转发给设备
            sendto(sockfd, data, strlen(data), 0,
                   (struct sockaddr *)(&dev_addr_array[dev_num]), sizeof(struct sockaddr_in));
        }
    }
    close(sockfd);
}

编译过程:

生成可执行文件:

输出结果:

client端发送和接受数据:

server端接收和发送数据:

 需要注意的是端口可以被多个进程使用的,数据却是唯一的,所以接收方和发送方端口尽量要唯一,不然数据发送出去了,接受方找不到。除此之外还需要注意的是发送一旦失败最好重新编译运行,因为发送失败就结束了,return NULL.最后要注意的就是send 你的IP地址和从哪个端口发送出去相当于命令,不能和对方的命令冲突否则可能会发送失败或无响应。

七、信号

进程间的基本通信方式除了上面的管道(pipe),有名管道(mkfifo),消息队列(msqid),磁盘映射(mmap),共享内存段(shmid),套接字(socket),还有最古老的通信方式信号。

信号的介绍:

信号是什么呢?他是表示消息的物理量,也可以说是运载消息的工具,是消息的载体

比如古代的战争中,边关守军发现敌人来袭,立即点燃烽火台,产生了滚滚浓烟,向远方的军队传递敌人入侵的消息,而滚滚浓烟就是信号,是一种光信号;

再比如两个人正常交流,声音在双方之间传递,使得双方能够理解对方的话,声音就是信号,是一种声信号;

而我们使用电子产品例如手机进行打电话,就是通过电信号来传递消息,信号多种多样,可以说信号无处不在。

在编程中,信号也可以是软件中断,它是在 软件层次上对中断机制的一种模拟,是一种异步通信的方式。

信号可以导致一个 正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。

信号的特点:

简单 不能携带大量信息 满足某个特设条件就会触发

一个完整的信号周期包括三个部分:

信号 的产生,信号在进程中的注册(执行信号处理函数),信号在进程中的注销。

通过shell命令查看信号的编号:kill -l

 其中 1-31 号信号称之为常规信号(也叫普通信号或标准 信号),34-64 称之为实时信号,驱动编程与硬件相关。名字上区别不大。而前 32 个名字各不相同

信号的产生:

1、 当用户按某些终端键时,将产生信号。 终端上按“Ctrl+c”组合键通常产生 中断信号 SIGINT 终端上按“Ctrl+\”键通常产生中断信号 SIGQUIT 终端上按 “Ctrl+z”键通常产生中断信号 SIGSTOP 等。

2、硬件异常将产生信号。 除数为 0,无效的内存访问等。这些情况通常由硬件检测到,并通知内核,然后内核产生 适当的信号发送给相应的进程。

3、软件异常将产生信号。 当检测到某种软件条 件已发生(如:定时器 alarm),并将其通知有关进程时,产生信号。

4、 调用系统 函数(如:kill、raise、abort)将发送信号。 注意:接收信号进程和发送信号进程 的所有者必须相同,或发送信号进程的所有者必须是超级用户。

5、运行 kill /killall 命令将发送信号。 此程序实际上是使用 kill 函数来发送信号。也常用此命 令终止一个失控的后台进程。

信号在进程中具有生命周期:注册、调用和消亡

信号的注册:

#include <signal.h>
typedef void(*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:
    注册信号处理函数(不可用于 SIGKILL、SIGSTOP 信号),即确定收到信号后处理
函数的入口地址。此函数不会阻塞。
参数:
    signum:信号的编号,这里可以填数字编号,也可以填信号的宏定义,可以通过命
令 kill - l("l" 为字母)进行相应查看。
    handler : 取值有 3 种情况:
        SIG_IGN:忽略该信号
        SIG_DFL:执行系统默认动
返回值:
    成功:第一次返回 NULL,下一次返回此信号上一次注册的信号处理函数的地址。如果需要使用此返回值,必须在前面先声明此函数指针的类型。
    失败:返回 SIG_ERR

 案例:signal.c

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

char *p;
void my_deal_func()
{
    if (p != NULL)
    {
        free(p);
        p = NULL;
        printf("使用终止信号后free释放内存的堆区空间\n");
    }
    _exit(-1);
    return;
}
// 使用自定义信号来释放内存的堆区空间
int main(int argc, char const *argv[])
{
    // 注册终止信号  SIGINT信号编号为2,程序终止(interrupt)信号,用户通过键盘上的CTRL+C,发出终止信号
    signal(SIGINT, my_deal_func);

    p = (char *)calloc(1, 128);
    printf("请输入数据:");
    fgets(p, sizeof(p), stdin);
    printf("输入的内容:%s\n", p);

    while (1)
        ;
        //因为有while循环,所有下面的程序都不会执行,目的是为了判断信号是否注册和调用
    free(p);
    p = NULL;
    printf("p指向的内存堆区空间被free释放的\n");
    return 0;
}

输出结果:正常不会结束,使用CTRL+C发送结束信号后,打印右边内容

 给某个运行的进程发送一个kill信号

可以通过kill函数:功能是通过pid给相关进程发送信号

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
功能:给指定进程发送指定信号(不一定杀死)
参数:
pid : 取值有 4 种情况 :
    pid > 0: 将信号传送给进程 ID 为 pid 的进程。
    pid = 0 : 将信号传送给当前进程所在进程组中的所有进程。
    pid = -1 : 将信号传送给系统内所有的进程。
    pid < -1 : 将信号传给指定进程组的所有进程。这个进程组号等于 pid 的绝
对值。
sig : 信号的编号,这里可以填数字编号,也可以填信号的宏定义,可以通过命令 k
ill - l("l" 为字母)进行相应查看。不推荐直接使用数字,应使用宏名,因为不同操作
系统信号编号可能不同,但名称一致。
返回值:
    成功:0
    失败:-1

案例:

kill.c

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

int main(int argc, char const *argv[])
{
    pid_t pid=fork();//创建子进程
    if(pid==0)//子进程
    {
        while (1)
        {
            printf("子进程%d正在执行任务A\n",getpid());
            sleep(1);
        }
        
    }
    else if(pid>0)
    {
        printf("我是父进程%d,我等待kill信号,准备回收子进程%d的资源\n",getppid(),pid);
        sleep(3);
        kill(pid,9);//这里的9是SIGKILL的编号,每个系统对于信号的编号对应的功能可能不同,
//可以使用宏
//kill(pid,SIGKILL);作用等同于kill(pid,9);
        pid=wait(NULL);
        printf("我收到了子进程%d的资源\n",pid);

    }
    return 0;
}

输出结果:

说明 kill(pid,9),pid是子进程的进程ID,9是SIGKILL信号的编号,该信号的功能是结束该进程。

raise函数:给当前进程组发送指定信号

#include <signal.h>
int raise(int sig);
功能:给当前进程发送指定信号(自己给自己发),等价于 kill(getpid(), sig)
参数:
    sig:信号编号
返回值:
    成功:0            
    失败:非 0 值

案例:

raise.c

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

int main(int argc, char const *argv[])
{
    int i = 0;
    while (1)
    {
        printf("只有我一个进程%d,我在执行特殊的任务\n", getppid());
        sleep(1);
        i++;
        if (i == 3)
        {
            printf("我运行了三次特殊任务,任务执行完毕\n");
            raise(9); // 9是SIGKILL信号的编号,raise函数执行后在终端返回“已杀死”,注意其返回值是0,已杀死是SIGKILL信号返回的
        }
    }

    return 0;
}

输出结果:

abort函数:给当前进程组发送异常信号


#include <stdlib.h>
void abort(void);
功能:给自己发送异常终止信号 6) SIGABRT,并产生 core 文件,等价于 kill(getpid
(), SIGABRT);
参数:无
返回值:无


//6是SIGABRT信号的编号,不同系统下信号编号有所变化
//Core文件其实就是内存的映像,当程序崩溃时,存储内存的相应信息,主用用于对程序进行调试。当程序崩溃时便会产生core文件,其实准确的应该说是core dump 文件,默认生成位置与可执行程序位于同一目录下,文件名为core.***,其中***是某一数字.需要注意的事core文件也有可能不会产生,或者转移到了其他目录

案例:abort.c

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

int main(int argc, char const *argv[])
{

    int i = 0;
    while (1)
    {
        printf("只有我%d一个进程,我在执行特殊的任务A\n", getppid());
        sleep(1);
        i++;
        if (i == 3)
        {
            printf("我执行了三次任务A,任务执行完毕\n");
            abort(); // 相当于SIGABRT信号,编号6,等价于kill(getpid(),SIGABRT);
        }
    }

    return 0;
}

输出结果:

alarm函数:定时器信号


#include <unistd.h>
unsigned int alarm(unsigned int seconds);
功能:
设置定时器(闹钟)。在指定 seconds 后,内核会给当前进程发送 14)SIGALRM 信
号。进程收到该信号,默认动作终止。每个进程都有且只有唯一的一个定时器。

取消定时器 alarm(0),返回旧闹钟余下秒数。
参数:
    seconds:指定的时间,以秒为单位
返回值:
    返回 0 或剩余的秒数

案例:alarm.c

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

int main(int argc, char const *argv[])
{
    //这里只定义了一个alarm函数,也就是说该进程的生命周期为6秒
    printf("我是唯一的进程%d,我在执行特殊的任务A\n", getpid());
    unsigned int result = alarm(6);
    printf("任务A第一次执行正常,剩余执行次数:%d\n", result+6);

    printf("任务A执行正常,剩余执行次数:%d\n", result);
    while (1)
        ;

    return 0;
}

输出解雇:

精度更高的定时器函数settimer(),精度可达微妙,可实现周期性

#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value, struct itim
erval *old_value);
功能:
    设置定时器(闹钟)。 可代替 alarm 函数。精度微秒 us,可以实现周期定时。
参数:
which:指定定时方式
    a) 自然定时:ITIMER_REAL → 14)SIGALRM 计算自然时间
    b) 虚拟空间计时(用户空间):ITIMER_VIRTUAL → 26)SIGVTALRM 只计算进
程占用 cpu 的时间
    c) 运行时计时(用户 + 内核):ITIMER_PROF → 27)SIGPROF 计算占用 cpu 及
执行系统调用的时间
old_value: 存放旧的 timeout 值,一般指定为 NULL
返回值:
    成功:0
    失败:-1


//struct itimerval结构体:
struct itimerval {
    struct timerval it_interval; // 闹钟触发周期
    struct timerval it_value; // 闹钟触发时间(第一次执行时间)
};

struct timeval {
    long tv_sec; // 秒
    long tv_usec; // 微秒
}
itimerval.it_value: 设定第一次执行 function 所延迟的秒数
itimerval.it_interval: 设定以后每几秒执行 function

 案例:settimer.c

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

// sig参数表示被触发的信号的编号
void my_func(int sig)
{
    printf("触发了编号为%d的信号\n", sig);
    return;
}
int main(int argc, char const *argv[])
{
    // 定义定时器的结构体变量new_t
    struct itimerval new_t;
    // 设置第一次触发时间为3秒
    new_t.it_value.tv_sec = 3;  // 3秒,秒定时
    new_t.it_value.tv_usec = 0; // 微秒定时

    // 设置周期触发时间为2秒
    new_t.it_interval.tv_sec = 2;  // 2秒,秒周期定时
    new_t.it_interval.tv_usec = 0; // 微秒周期定时

    // 注册信号,信号在进程中注册后,对信号的处理方式为自定义函数处理
    signal(SIGALRM, my_func);

    // 开始定时
    setitimer(ITIMER_REAL, &new_t, NULL); // ITIMER_REAL表示自然计时,NULL表示不存放旧值
    while (1)
        ;

    return 0;
}

输出结果:

14是SIGALRM信号的编号,第一次执行会等待3秒后打印结果,之后每2秒打印一次结果

sigaction函数:signal函数的升级版,可以设置对信号的处理方式

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction
*oldact);
功能:
    检查或修改指定信号的设置(或同时执行这两种操作)。
参数:
    signum:要操作的信号。
    act: 要设置的对信号的新处理方式(传入参数)。
    oldact:原来对信号的处理方式(传出参数)。
    如果 act 指针非空,则要改变指定信号的处理方式(设置),如果 oldact 指针非
空,则系统将此前指定信号的处理方式存入 oldact。
返回值:
    成功:0
    失败:-1

//struct sigaction 结构体:
struct sigaction {
    void(*sa_handler)(int); //旧的信号处理函数指针
    void(*sa_sigaction)(int signum, siginfo_t *info, void *contex); //新的信号处理函数指针
    sigset_t sa_mask; //信号阻塞集
    int sa_flags; //信号处理的方式
    void(*sa_restorer)(void); //已弃用
};

void(*sa_sigaction)(int signum, siginfo_t *info, void *contex)//新的信号处理函数指针
参数说明:
    signum:信号的编号。
    info:记录信号发送进程信息的结构体。
    context:可以赋给指向 ucontext_t 类型的一个对象的指针,以引用在传递信号
时被中断的接收进程或线程的上下文

案例:sigaction.c

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

char *p;
void my_deal_func()
{
    if (p != NULL)
    {
        free(p);
        p = NULL;
        printf("使用终止信号后free释放内存的堆区空间\n");
    }
    return;
}
// 使用自定义信号来释放内存的堆区空间
int main(int argc, char const *argv[])
{

    struct sigaction sact;

    //保存自定义函数的入口地址
    sact.sa_handler=my_deal_func;

    //将所有信号添加到集合中
    sigset_t set;
    sigfillset(&set);
    sact.sa_mask=set;

    //信号处理方式
    sact.sa_flags |=SA_RESETHAND;

    // 注册终止信号  SIGINT信号编号为2,程序终止(interrupt)信号,用户通过键盘上的CTRL+C,发出终止信号
    sigaction(SIGINT,&sact,NULL);

    p = (char *)calloc(1, 128);
    printf("请输入数据:");
    fgets(p, sizeof(p), stdin);
    printf("输入的内容:%s\n", p);

    while (1)
        ;
    // 因为有while循环,所有下面的程序都不会执行,目的是为了判断信号是否注册和调用
    free(p);
    p = NULL;
    printf("p指向的内存堆区空间被free释放的\n");
    return 0;
}

输出结果:第一次使用CTRL+C来释放p指向的堆区空间,此时再输入没有输出(因为堆区空间被释放了),第二次使用CTRL+C来终止进程,由信号来终止

PCB(process control block),系统内核会为每一个进程分配一个PCB。

在 PCB 中有两个非常重要的信号集。一个称之为“阻塞信号集”,另一个称之为“未 决信号集”。 这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我 们直接对其进行位操作。而需自定义另外一个集合(自定义信号集),借助信号集操作函数来对 PCB 中的这两个信号集进行修改。

阻塞信号集:就是一堆信号受到阻塞,不让其被相关函数处理掉,把这些阻塞的信号集合在一块,称为阻塞信号集

未决信号集:也是一堆信号,只是一堆没有被处理掉的信号,把这些没有处理掉的信号集合在一块,称为未决信号集

 因为不能直接操作信号集,所以需要自定义信号集

信号集和信号阻塞集:

//信号集 所有信号的集合
#include <signal.h>
int sigemptyset(sigset_t *set); //将 set 集合置空
int sigfillset(sigset_t *set); //将所有信号加入 set 集合
int sigaddset(sigset_t *set, int sign); //将 sign 信号加入到 set 集合
int sigdelset(sigset_t *set, int sign); //从 set 集合中移除 sign 信号
int sigismember(const sigset_t *set, int sign); //判断信号是否存在

//信号阻塞集 所有被阻塞的信号
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
功能:
    检查或修改信号阻塞集,根据 how 指定的方法对进程的阻塞集合进行修改,新的信
号阻塞集由 set 指定,而原先的信号阻塞集合由 oldset 保存。
参数:
    how : 信号阻塞集合的修改方法,有 3 种情况:
        SIG_BLOCK:向信号阻塞集合中添加 set 信号集,新的信号掩码是 set 和旧信
号掩码的并集。 相当于 mask = mask|set。
        SIG_UNBLOCK:从信号阻塞集合中删除 set 信号集,从当前信号掩码中去除 s
et 中的信号。相当于 mask = mask & ~ set。
        SIG_SETMASK:将信号阻塞集合设为 set 信号集,相当于原来信号阻塞集的内
容清空,然后按照 set 中的信号重新设置信号阻塞集。相当于 mask = set。
set : 要操作的信号集地址。
若 set 为 NULL,则不改变信号阻塞集合,函数只把当前信号阻塞集合保存到
oldset 中。
oldset : 保存原先信号阻塞集地址
返回值:
    成功:0,
    失败:-1,失败时错误代码只可能是 EINVAL,表示参数 how 不合法

案例:set_mask.c

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

char *p;
void my_deal_func()
{
    if (p != NULL)
    {
        free(p);
        p = NULL;
        printf("使用终止信号后free释放内存的堆区空间\n");
    }
    _exit(-1);
    return;
}
// 使用自定义信号来释放内存的堆区空间
int main(int argc, char const *argv[])
{
    // 注册终止信号  SIGINT信号编号为2,程序终止(interrupt)信号,用户通过键盘上的CTRL+C,发出终止信号
    signal(SIGINT, my_deal_func);

    sigset_t set;      // 设置信号集
    sigemptyset(&set); // 清空集合
    // 将信号SIGINT加入到信号集中
    sigaddset(&set, SIGINT);

    // 将信号集添加到阻塞集中
    sigprocmask(SIG_BLOCK, &set, NULL);
    printf("信号集已经添加到阻塞集中,持续时间9秒\n");

    p = (char *)calloc(1, 128);
    printf("请输入数据:");
    fgets(p, sizeof(p), stdin);
    printf("输入的内容:%s\n", p);
    sleep(9);
    sigprocmask(SIG_UNBLOCK, &set, NULL);
    printf("信号集从阻塞集中删除\n");

    while (1)
        ;
    // 因为有while循环,所有下面的程序都不会执行,目的是为了判断信号是否注册和调用
    free(p);
    p = NULL;
    printf("p指向的内存堆区空间被free释放的\n");
    return 0;
}

输出结果:需要注意的是sleep函数只是模拟时间,不会终结进程,而定时器是不论进程执行什么任务,到时间就结束。因此有fgets阻塞函数,需要从解除阻塞函数开始计时9秒。

 进程间的通信远远不止这些,还有其他的方式来通信。不过最基本的进程之间通信的方式应该是以上的七种。

就像自然界一样,编程世界也是多姿多彩的~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值