29_进程基础

进程基础

介绍

进程[动态]指正在运行的程序(保持在存储介质上的,经过编译的,可执行的二进制文件[静态]),换句话说,进程是程序的执行过程。

如下图示,进程是资源分配的最小单位, 在某一时刻cpu只运行一个进程,通过时间片轮转切换来实现很多进程看似的"同时运行"。

可以通过“ps ” 或“top” 等命令查看正在运行的进程, 线程是系统的最小调度单位, 一个进程可以拥有多个线程, 同一进程里的线程可以共享此进程的同一资源 。

image-20240404232628378

每个进程都有一个唯一的标识符, 既进程 ID, 简称 pid

进程间通信不能直接应用层交流,得通过linux内核来进行交流,通过创建内核对象进行交流

image-20240404233843769

进程间的通信的几种方法?
1 管道通信: 有名管道, 无名管道
2 信号通信: 信号的发送, 信号的接受, 信号的处理
3 IPC 通信: 共享内存, 消息队列, 信号灯
4 Socket 通信
进程的三种基本状态以及转换:

image-20240404232658640

进程创建

​ 所有的进程都是由其他进程创建(除了 pid 为 0 号的 idle 进程) , pid 号为 1 的 init 进程是系统启动后运行的第一个进程, 是所有进程的父进程, init 进程会初始化一部分系统服务, 创建其他进程。
​ 创建新进程的那个进程称为父进程, 新进程称为子进程, 父进程和子进程拥有相同的代码段数据段,有各自独立的地址空间。 采用写时拷贝技术, 即创建的新进程不会立即把父进程的资源空间复制过来, 只有在修改时才会复制资源, 另外父进程挂起的信号和文件锁不会被子进程继承。
​ 子进程结束后, 它的父进程要回收它的资源, 否则就会成为僵尸进程 。
​ 如果父进程先结束, 子进程会被 init 进程收养, 称为孤儿进程。

image-20240404234545693

代码

fork.c

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

int main(int argc, char const *argv[])
{
    pid_t pid;
    pid = fork();
    if(pid < 0)
    {
        printf("fork is error\n");
        return -1;
    }
    //父进程
    if(pid > 0)
    {
        printf("This is parent, this pid is %d\n", getpid());
    }else if(pid == 0)
    {
        printf("This is child, this pid is %d, this parent pid is %d\n", getpid(), getppid());
    }


    return 0;
}

运行结果

image-20240405000248725

为什么父进程和子进程都打印了?

image-20240405000929122

子进程是父进程的拷贝,所以子进程的内容和父进程一样,所以分别打印的父进程和子进程

那么父进程和子进程有什么区别吗?

1 父进程和子进程的pid号不一样,通过fork()函数得到的值不同

2 执行的位置不一样

image-20240405001408678

​ 父进程从函数开始处执行, 子进程从fork()处执行

父进程和子进程谁先执行?

不确定,看谁先抢到cpu资源

exec 函数族

​ 用 fork 函数创建子进程后, 子进程往往要调用一种 exec 函数以执行另一个程序, 该子进程被新的程序替换, 改变地址空间, 进程映像和一些属性, 但是 pid 号不变。 exec 函数详解如下表所示:

image-20240405003406690

以下函数都是根据 execve 实现

int execl(const char *path, const char arg, .../ (char *) NULL */);

​ path参数指定了要执行的可执行文件的路径。

​ arg参数表示传递给可执行文件的命令行参数。

​ NULL参数标志着参数列表的结束。int execlp(const char *file, const 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[]);

为什么exec函数族可以让子进程执行不同的程序?

exec函数族特点: 换核不换壳

当进程调用exec函数时, 进程的用户空间的代码和数据空间完全被新的程序所替代

调用exec函数不会创建新的进程, 进程id号不变

image-20240405150313680

exec函数族的使用场景

在Linux中使用exec函数族主要有以下两种情况:
1.当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用任何exec函数族让自己重生。
2.如果一个进程想执行另一个程序,那么它就可以调用fork函数新建一个进程,然后调用任何一个exec函数使子进程重生。

使用execl函数

fork.c

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

int main(int argc, char const *argv[])
{
    pid_t pid;
    int i = 0;
    pid = fork();
    if (pid < 0)
    {
        printf("fork is error\n");
        return -1;
    }

    if (pid > 0) // 父进程
    {
        printf("This is parent, this pid is %d\n", getpid());
    }
    else if (pid == 0) // 子进程
    {
        printf("This is child, this pid is %d, this parent pid is %d\n", getpid(), getppid());
        execl("/home/nfs/hello", "hello", NULL);
        exit(1); // 退出进程, 参数: 0->成功; 1->失败
    }

    i++;
    printf("i is %d\n", i);
    return 0;
}

运行结果

image-20240405151845180

可以看到子进程的 i 没有打印出来

还可以调用shell命令

image-20240405152638626

运行结果

image-20240405152738643

ps 和 kill 命令

ps 命令: ps 命令可以列出系统中当前运行的那些进程。

​ 命令格式: ps [参数]
​ 命令功能: 用来显示当前进程的状态
​ 常用参数: aux

image-20240405153603366

字段解释:

​ USER:创建进程的用户
​ PID:进程ID
​ %CPU:进程占用CPU的百分比
​ %MEM:进程占用物理内存的百分比
​ VSZ:进程占用虚拟内存的大小(单位KB)
​ RSS:进程占用实际物理内存的大小(单位KB)
​ TTY:进程在哪个终端运行。
​ STAT:进程状态
​ START:进程开始启动的时间
​ TIME:进程使用的CPU(运算)时间
​ COMMAND:调用进程的命令

通过管道查看aux进程

image-20240405153917981

|是管道

grep是一个可以利用”正则表达式”进行”全局搜索”的工具*,grep会在文本文件中按照指定的正则进行全局搜索,并将搜索出的行打印出来

kill 命令: kill 命令用来杀死进程

​ 举例: kill -9(SIGKILL) PID

进程的状态

D: 无法中断的休眠状态 (通常 IO 的进程)
R: 正在执行中
S: 静止状态
T: 暂停执行
Z: 不存在但暂时无法消除
W: 没有足够的记忆体分页可分配
<: 高优先序的行程
N: 低优先序的行程
L: 有记忆体分页分配并锁在记忆体内 (实时系统或捱 A I/O)

如下所示, 使用命令“ps aux | grep a.out” 查找到./a.out 的进程号为 3179, 然后输入“kill -9 3179” 结束此进程,

image-20240405155130714

完成后再次输入查找命令“ps aux | grep a.out” , 没有发现 3179 号进程。

image-20240405155143211

孤儿进程与僵尸进程

孤儿进程: 父进程结束以后, 子进程还未结束, 这个子进程就叫做孤儿进程。

​ 特点: 孤儿进程会被系统的init进程领养(pid=1)

示例代码:

​ 在程序中, 创建的子进程变为孤儿进程。

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

int main(int argc, char const *argv[])
{
    pid_t pid;
    pid = fork(); // 创建一个子进程
    if(pid < 0)
    {
        printf("error\n");
    }
    if(pid > 0) //父进程 让父进程先结束,并打印进程 PID
    {
        printf("pid is %d\n", getpid());
    }
    if(pid == 0) //子进程,让子进程等待一会,让父进程先结束,并打印子进程的父进程的 pid
    {
        sleep(2);
        printf("parent pid is %d\n", getppid());
    }
    return 0;
}

image-20240405160902136

如上图所示, 子进程中打印的父进程的进程号和父进程的进程号是不一样的, 说明创建的子进程变成了孤儿进程, 此进程被系统的 init 进程"领养"了, 如下图所示:

image-20240405160935459

僵尸进程: 子进程结束以后, 父进程还在运行, 但是父进程不去释放进程控制块, 这个子进程就叫做僵尸进程。

实验代码 :

​ 在程序中, 子进程变为僵尸进程 。

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

int main(int argc, char const *argv[])
{
    pid_t pid;
    pid = fork();// 创建一个子进程
    if (pid < 0)
    {
        printf("error\n");
    }
    if (pid > 0)//父进程 让父进程先结束, 并打印进程 PID
    {
        printf("pid is %d\n", getpid());
        while (1)
        {
        }
    }
    if (pid == 0)//子进程,让子进程先结束
    {
        printf("parent pid is %d\n", getppid());
        exit(0);
    }
    return 0;
}

编译运行, 如下图所示:

image-20240405161112488

再打开另一个终端, 查看此进程如下图所示为僵尸进程。

image-20240405161122387

wait函数

wait()函数一般用在父进程中等待回收子进程的资源, 而防止僵尸进程的产生。

image-20240405172330569

与 wait 函数的参数有关的俩个宏定义:

WIFEXITED(status): 如果子进程正常退出, 则该宏定义为真
WEXITSTATUS(status): 如果子进程正常退出, 则该宏定义的值为子进程的退出值。

示例代码:

在程序中, 使用 wait() 函数, 防止僵尸进程的产生

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

int main(int argc, char const *argv[])
{
    pid_t pid;
    pid = fork(); // 创建一个子进程
    if (pid < 0)
    {
        printf("error\n");
    }
    if (pid > 0)
    {
        int status;
        wait(&status);
        if (WIFEXITED(status) == 1)
        {
            printf("return value is %d\n", WEXITSTATUS(status));
        }
    }
    if (pid == 0)
    {
        sleep(2);
        printf("This is child\n");
        exit(6);
    }
    return 0;
}

运行结果

image-20240405173446208

守护进程

什么是守护进程?

​ 守护进程(daemon)是一类在后台运行的特殊进程, 用于执行特定的系统任务。 很多守护进程在系统引导的时候启动, 并且一直运行直到系统关闭。 另一些只在需要的时候才启动, 完成任务后就自动结束。

​ 用户使守护进程独立于所有终端是因为, 在守护进程从一个终端启动的情况下, 这同一个终端可能被其他的用户使用。 例如, 用户从一个终端启动守护进程后退出, 然后另外一个人也登录到这个终端。 用户不希望后者在使用该终端的过程中, 接收到守护进程的任何错误信息。 同样, 由终端键入的任何信号(例如中断信号)也不应该影响先前在该终端启动的任何守护进程的运行。虽然让服务器后台运行很容易(只要 shell命令行以&结尾即可), 但用户还应该做些工作, 让程序本身能够自动进入后台, 且不依赖于任何终端。
​ 守护进程没有控制终端, 因此当某些情况发生时, 不管是一般的报告性信息, 还是需由管理员处理的紧急信息, 都需要以某种方式输出。Syslog 函数就是输出这些信息的标准方法, 它把信息发送给 syslogd 守护进程。

怎么创建一个守护进程?

基本要求:

1 必须作为我们 init 进程的子进程(孤儿进程)

2 不跟控制终端交互。
步骤:
​ 1.使用 fork 函数创建一个新的进程, 然后让父进程使用 exit 函数直接退出(必须要的)

​ 2.调用 setsid 函数。 (必须要的)

​ 函数用法:

include <unistd.h>

pid_t setsid(void);

​ 解释: 虽然父进程退出了,但是子进程拷贝了父进程的进程组,会话,控制终端等,那么这个函数就是用来摆脱原来会话,进程组,控制终端的控制,生成新的会话,抛弃控制终端,在新的会话内创建新的进程组,调用的进程就成了首进程(进程组中的唯一进程)image-20240405174520014 3.调用 chdir 函数, 将当前的工作目录改成根目录, 增强程序的健壮性。 (不是必须要的)

​ 函数用法:

include <unistd.h>

int chdir(const char *path); //path->路径
int fchdir(int fd);

​ 解释: 如果你的程序放在U盘上, 那么在程序运行时,把U盘拔掉, 那这个程序就崩溃了, 所以要改变当前的工作目录

​ 4.重设我们 umask 文件掩码, 增强程序的健壮性和灵活性(不是必须要的)

​ 函数用法:

include <sys/types.h>

include <sys/stat.h>

mode_t umask(mode_t mask);

​ 解释: 子进程会继承父进程的掩码, 会有bug

​ 5.关闭文件描述符, 节省资源(不是必须要的)

​ 解释: 关闭0(标准输入),1(标准输出),2 (标准出错)这三个文件描述符, 为了节省资源, 守护进程不跟终端联系了,所以没什么用处

​ 6.执行我们需要执行的代码(必须要的)

实例代码

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

int main(int argc, char const *argv[])
{
    pid_t pid;
    // 步骤一: 创建一个新的进程
    pid = fork();
    if (pid > 0) // 父进程直接退出
    {
        exit(0);
    }
    if (pid == 0)
    {
        // 步骤二: 调用 setsid 函数摆脱控制终端
        setsid();
        // 步骤三: 更改工作目录
        chdir("/"); // 工作目录改到根目录下
        // 步骤四: 重新设置 umask 文件掩码
        umask(0);
        // 步骤五:关闭 0 1 2 三个文件描述符
        close(0);
        close(1);
        close(2);
        // 步骤六: 执行我们要执行的代码
        while (1)
        {
        }
    }
    return 0;
}

运行结果

image-20240405181525974

进程间通信

​ 进程间的通信应用也是很广泛的, 比如后台进程和 GUI 界面数据传递, 发送信号关机, Ctrl+C 终止正在运行的程序等。
Linux 进程间通信机制分三类: 数据交互, 同步, 信号。 理解了这些机制才能灵活运用操作系统提供的IPC 工具。
​ 本章以常用的管道(包括有名管道和无名管道), System V IPC(消息队列, 共享内存, 信号灯) , 套接字( UNIX 域套接字和网络套接字) 为例来说明 Linux 进程通信常用的方法, 本文档中介绍的只是一小部分,如果想深入了解可以去翻看专业的书籍。

无名管道

image-20240405182448170

无名管道是最古老的进程通信方式, 有如下两个特点:

1 只能用于有关联的进程间数据交互, 如父子进程, 兄弟进程, 子孙进程, 在目录中看不到文件节点, 读写文件描述符存在一个 int 型数组中。

2 只能单向传输数据, 即管道创建好后, 一个进程只能进行读操作, 另一个进程只能进行写操作,读出来字节顺序和写入的顺序一样。

pipe函数

image-20240405181923837

image-20240405181932264

无名管道使用步骤:

1 调用 pipe()创建无名管道;

2 fork()创建子进程, 一个进程读, 使用 read(), 一个进程写, 使用 write()。

​ 注意:

1得在fork()之前创建管道,保证两个进程使用同一个管道

2当管道中没有数据时,读取管道会被阻塞

示例代码

pipe.c

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

int main(int argc, char const *argv[])
{
    pid_t pid;
    int fd[2]; // pd[0]读 pd[1]写
    char buf[32] = {0};
    pipe(fd);
    printf("fd[0] is %d\n", fd[0]);
    printf("fd[1] is %d\n", fd[1]);
    pid = fork();
    if (pid < 0)
    {
        printf("error\n");
    }
    if (pid > 0)
    {
        int status;
        close(fd[0]);
        write(fd[1], "hello", 5);
        close(fd[1]);
        wait(&status);
        exit(0);
    }
    if (pid == 0)
    {
        close(fd[1]); // 关闭写端
        read(fd[0], buf, sizeof(buf));
        printf("buf is %s\n", buf);
        close(fd[0]); // 关闭读端
        exit(0);
    }
    return 0;
}

运行结果

image-20240405185511207

有名管道

​ 有名管道中可以很好地解决在无关联进程间数据交换的要求, 并且由于它们是存在于文件系统中的, 这也提供了一种比匿名管道更持久稳定的通信办法。

有名管道在一些专业书籍中叫做命名管道, 它的特点是:
1.可以使无关联的进程通过 fifo 文件描述符进行数据传递;
2.单向传输有一个写入端和一个读出端, 操作方式和无名管道相同。

我们使用 mkfifo()函数创建有名管道。 函数详解如下所示:

image-20240405185619712

有名管道使用步骤:
1使用 mkfifo()创建 fifo 文件描述符。

2打开管道文件描述符。

3通过读写文件描述符进行单向数据传输。

使用命令创建管道文件

image-20240405211459003

管道文件fifo大小为0不占空间

权限prw的p就是管道文件

access 函数

用于检查调用进程是否具有访问指定文件的权限

access 函数原型如下:

include <unistd.h>

int access(const char *pathname, int mode);

参数说明:

pathname:指向一个字符串,表示要检查权限的文件的路径。
mode:指定要检查的权限类型,可以是以下值的组合:
F_OK:用于检查文件是否存在。
R_OK:用于检查是否具有读取权限。
W_OK:用于检查是否具有写入权限。
X_OK:用于检查是否具有执行权限。
返回值:

​ 如果指定的权限检查成功,则函数返回 0。
​ 如果权限检查失败,则函数返回 -1,并设置 errno 为相应的错误代码。
​ 需要注意的是,access 函数检查的是调用进程的实际用户 ID 和实际组 ID 对文件的访问权限,而不是有效用户 ID 和有效组 ID。这意味着,如果一个进程以特权用户(如 root)运行,但实际用户 ID 仍然是普通用户,那么 access 函数将根据普通用户的权限进行检查。

示例代码:

创建两个无关联的进程, 一个进程创建有名管道并写数据, 另一个进程通过管道读数据。

fifo_write.c

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

int main(int argc, char const *argv[])
{
    int ret;
    int fd;
    char buf[32] = {"abcdefg"};
    if (argc < 2)
    {
        printf("Usage:%s <fifo name> \n", argv[0]);
        return -1;
    }
    if (access(argv[1], F_OK) == -1) // 文件不存在,则创建
    {
        ret = mkfifo(argv[1], 0666);
        if (ret < 0)
        {
            printf("mkfifo is error\n");
            return -2;
        }
        printf("mkfifo is ok\n");
    }
    fd = open(argv[1], O_WRONLY);
    if (fd < 0)
    {
        printf("open is error\n");
        return -3;
    }
    while (1)
    {
        sleep(1);
        write(fd, buf, strlen(buf));
    }
    close(fd);
    return 0;
}

fifo_read.c

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

int main(int argc, char const *argv[])
{
    int fd;
    char buf[32] = {0};
    ssize_t ret;
    if (argc < 2)
    {
        printf("Usage:%s <fifo name> \n", argv[0]);
        return -1;
    }
    fd = open(argv[1], O_RDONLY);
    if (fd < 0)
    {
        printf("open is error\n");
        return -2;
    }
    while (1)
    {
        sleep(1);
        read(fd, buf, sizeof(buf));
        printf("buf is %s\n", buf);
        memset(buf, 0, sizeof(buf));
    }

    close(fd);
    return 0;
}

运行结果

在Ubuntu 下编译 fifo_read.c, 并运行如下图所示:

image-20240405234709605

读没数据的管道会被阻塞

然后重新打开一个窗口, 编译 fifo_write.c 并运行如下图所示:

image-20240405234717378

fiforead 进程可以看到从管道读出的数据:

image-20240405234801847

信号

image-20240406152132495

在 Ubuntu 终端输入 kill -l, 查看所有的信号。

image-20240406151019388

image-20240406151043265

image-20240406151119951

信号发送

信号是 Linux 系统响应某些条件而产生的一个事件, 接收到该信号的进程会执行相应的操作。
信号的产生有三种方式:
1)由硬件产生, 如从键盘输入 Ctrl+C 可以终止当前进程
2)由其他进程发送, 如可在 shell 进程下, 使用命令 kill -信号标号 PID, 向指定进程发送信号。
3)异常, 进程异常时会发送信号

下面是几个常用的函数:

kill

image-20240406151149818

image-20240409173347256

raise

image-20240409173357588

alarm

image-20240406151221322

示例代码

实验 1 代码: 在程序中实现: 自己给自己发送信号。

raise.c

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

int main(int argc, char const *argv[])
{
    printf("raise before\n");
    raise(9);
    printf("raise after\n");
    return 0;
}

运行结果

image-20240406153034231

实验 2 代码 kill.c 发送信号。

kill.c

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

int main(int argc, char const *argv[])
{
    pid_t pid;
    int sig;
    if(argc < 3)
    {
        printf("Usage:%s <pid_t> <signal>\n", argv[0]);
        return -1;
    }
    sig = atoi(argv[2]); //字符串转整形
    pid = atoi(argv[1]);
    kill(pid, sig);
    return 0;
}

test.c

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

int main(int argc, char const *argv[])
{
    while (1)
    {
        sleep(1);
        printf("hello world\n");
    }
    return 0;
}

编译运行 test, 如下图所示, 进程会循环打印 hello world。

image-20240406154037292

重新打开另一个窗口, 编译 kill.c, 然后查看 test 进程的 pid 号, 运行测试如下图所示;

image-20240406154112251

与此同时, 显示 test 的窗口显示, test 进程被杀死, 如下图所示

image-20240406154135374

实验 3 代码 alarml.c

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

int main(int argc, char const *argv[])
{
    int i;
    alarm(3);
    while (1)
    {
        sleep(1);
        i++;
        printf("i = %d\n", i);
    }
    return 0;
}

编译 alarm.c,并运行。 如下图所示, 设定的时间(3 秒) 超过后产生 SIGALARM 信号, 默认动作是终止进程。

image-20240406165642093

信号接收

接收信号: 如果要让我们接收信号的进程可以接收到信号, 那么这个进程就不能停止。 让进程不停止有三种方法:
 while
 sleep
 pause

方法一:

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

int main(int argc, char const *argv[])
{
    while (1)
    {
        sleep(1);
        printf("hello world\n");
    }
    
    return 0;
}

编译运行结果如下所示, 按 ctrl+C 会发送 SIGINT 信号:

image-20240406170228297

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

int main(int argc, char const *argv[])
{
    sleep(60);
    return 0;
}

编译运行, 如下图所示, 会休眠 60s.

image-20240406170240567

方法三

pause

函数详解如下:

image-20240406170258749

pause.c

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

int main(int argc, char const *argv[])
{
    printf("pause before\n");
    pause();
    printf("pause after\n");
    return 0;
}

编译程序并运行, 如下图所示:

image-20240406170513017

输入以下命令查看进程, 如下图所示:

image-20240406170521241

按 ctrl+C 键, pause 进程终止, 再次查看 pause 的进程, 如下图所示:

image-20240406170540793

信号处理

信号是由操作系统来处理的, 说明信号的处理在内核态。 信号不一定会立即被处理, 此时会储存在信号的信号表中。

处理过程示意图:

image-20240406181254175

由上图中可看出信号有三种处理方式:
1.默认方式(通常是终止进程) ,
2.忽略, 不进行任何操作。
3.捕捉并处理调用信号处理器(回调函数形式)

signal

image-20240406181323365

实验 1 代码实现信号忽略

signal.c

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

int main(int argc, char const *argv[])
{
    signal(SIGINT, SIG_IGN); //参数: 用户产生中断(ctrl+c); 忽略
    while (1)
    {
        printf("wait signal\n");
        sleep(1);
    }
    
    return 0;
}

编译运行程序, 如下图所示, 当我们按下 ctrl+C 键的时候, 信号被忽略。

image-20240406182622123

实验 2: 代码实现采用系统默认方式处理该信号

signal.c

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

int main(int argc, char const *argv[])
{
    signal(SIGINT, SIG_DFL); //参数: 用户产生中断(ctrl+c); 终止
    while (1)
    {
        printf("wait signal\n");
        sleep(1);
    }
    
    return 0;
}

编译运行程序, 如下图所示, 按 ctrl+c 可以终止程序

image-20240406182822272

实验 3 代码实现捕获到信号后执行此函数内容

signal.c

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

void myfun(int sig)
{
    if(sig == SIGINT)
    {
        printf("get sigint\n");
    }
}

int main(int argc, char const *argv[])
{
    signal(SIGINT, myfun); //参数: 用户产生中断(ctrl+c); 回调函数
    while (1)
    {
        printf("wait signal\n");
        sleep(1);
    }
    
    return 0;
}

编译运行程序如下图所示,当我们按下 ctrl+c 时, 显示 myfun 函数里面的打印信息。

image-20240406183114828

虽然调用了回调函数,但是并未终止

我们再打开另一个终端, 输入如下图所示的命令也可以实现同样的效果。

image-20240406183125131

共享内存

​ 共享内存, 顾名思义就是允许两个不相关的进程访问同一个逻辑内存, 共享内存是两个正在运行的进程之间共享和传递数据的一种非常有效的方式。 不同进程之间共享的内存通常为同一段物理内存。 进程可以将同一段物理内存连接到他们自己的地址空间中, 所有的进程都可以访问共享内存中的地址。 如果某个进程向共享内存写入数据, 所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。

image-20240406184139379

​ Linux 操作系统的进程通常使用的是虚拟内存, 虚拟内存空间是有由物理内存映射而来的。 System V 共享内存能够实现让两个或多个进程访问同一段物理内存空间, 达到数据交互的效果。

image-20240406183502439

共享内存和其他进程间数据交互方式相比, 有以下几个突出特点:

1 速度快, 因为共享内存不需要内核控制, 所以没有系统调用。 而且没有向内核拷贝数据的过程,
2 所以效率和前面几个相比是最快的, 可以用来进行批量数据的传输, 比如图片。
3 没有同步机制, 需要借助 Linux 提供其他工具来进行同步, 通常使用信号灯。
使用共享内存的步骤:
​ 1.调用 shmget()创建共享内存段 id,
​ 2.调用 shmat()将 id 标识的共享内存段加到进程的虚拟地址空间,
​ 3.访问加入到进程的那部分映射后地址空间, 可用 IO 操作读写。
常用 API 如下:

shmget函数image-20240406183601974

参数key: IPC_PRIVATE或者是ftok函数的返回值

ftok函数

image-20240406183609241

proj_id 的取值范围是0-255之间的数值

shmat函数

image-20240406211425849

shmat函数作用: 将共享内存映射到用户空间的地址中去

image-20240406210341131

操作的时候不用进内核里,可以直接在用户空间操作

所以共享内存是进程间共享数据效率最高的一种方法

shmdt函数

image-20240406183625483

image-20240406183634682

shmctl函数

image-20240406211531850

ipcs命令

​ 用于报告Linux中进程间通信设施的状态,显示的信息包括消息列表、共享内存和信号量的信息。

可选项:

​ -a:显示全部可显示的信息;
​ -q:显示活动的消息队列信息;
​ -m:显示活动的共享内存信息;
​ -s:显示活动的信号量信息。

ipcrm命令

​ 用于删除消息队列、信号集或共享内存标识。

可选项:
-m :删除共享内存标识SharedMemoryID。与SharedMemoryID相关联的共享内存段和数据结构将在最后一次拆离操作后被删除。
-M :删除用关键字SharedMemoryKey创建的共享内存标识。与其相关的共享内存段和数据结构段将在最后一次拆离操作后被删除。
-q :删除消息队列标识MessageID以及与其相关的消息队列和数据结构。
-Q :删除由关键字MessageKey创建的消息队列标识以及与其相关的消息队列和数据结构。
-s :删除信号量标识SemaphoreID以及与其相关的信号量集和数据结构。
-S :删除由关键字SemaphoreKey创建的信号标识以及与其相关的信号量集和数据结构。

实验代码:

在程序中, 创建共享内存。

shmget.c

#include<stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>

int main(int argc, const char* argv[]) {
    int shmid;
    shmid = shmget(IPC_PRIVATE, 1024, 0777);
    if(shmid < 0)
    {
        printf("shmget is error\n");
        return -1;
    }
    printf("shmget is ok and shmid is %d\n", shmid);
    return 0;
}

编译运行程序如下图所示:

image-20240406191312057

输入以下命令查看到创建的共享内存段的 id 和上面程序获取到的共享内存段的 id 是一样的。

image-20240406191325398

可以使用ipcrm命令删除这个共享内存

ipcrm -m 11927554

实验代码

在程序中, 父子进程通过共享内存通信。

shmget.c

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

int main(int argc, const char* argv[]) {
    int shmid;
    key_t key;
    pid_t pid;
    char *s_addr, *p_addr;

    key = ftok("a.c", 'a');
    shmid = shmget(key, 1024, 0777 | IPC_CREAT);
    if(shmid < 0)
    {
        printf("shmget is error\n");
        return -1;
    }
    printf("shmget is ok and shmid is %d\n", shmid);
    pid = fork();
    if(pid > 0)
    {
        p_addr = shmat(shmid, NULL, 0); //映射共享内存到用户空间
        strncpy(p_addr, "hello", 5);
        wait(NULL);
        exit(0);
    }
    if(pid == 0)
    {
        sleep(2);
        s_addr = shmat(shmid, NULL, 0);
        printf("s_addr is %s\n", s_addr);
        exit(0);
    }
    return 0;
}

编译运行程序如下图所示:

image-20240406212855568

​ 优点: 我们可以看到使用共享内存进行进程之间的通信是非常方便的, 而且函数的接口也比较简单,数据的共享还使进程间的数据不用传送, 而是直接访问内存, 加快了程序的效率。
​ 缺点: 共享内存没有提供同步机制, 这使得我们在使用共享内存进行进程之间的通信时, 往往需要借助其他手段来保证进程之间的同步工作。

消息队列

​ System V IPC 包含三种进程间通信机制, 有消息队列, 信号灯(也叫信号量) , 共享内存。 此外还有 SystemIPC 的补充版本 POSIX IPC, 这两组 IPC 的通信方法基本一致, 本章以 System V IPC 为例介绍 Linux 进程通信机制。
​ 可以用命令“ipcs” 查看三种 IPC, “ipcrm” 删除 IPC 对象。 在 i.MX6ULL 终结者开发板终端输入“ipcs”查看系统中存在的 IPC 信息:

image-20240406213333279

这些 IPC 对象存在于内核空间, 应用层使用 IPC 通信的步骤为:

image-20240406213341371

1 获取 key 值, 内核会将 key 值映射成 IPC 标识符, 获取 key 值常用方法:
(1) 在 get 调用中将 IPC_PRIVATE 常量作为 key 值。
(2) 使用 ftok()生成 key。
2 执行 IPC get 调用, 通过 key 获取整数 IPC 标识符 id, 每个 id 表示一个 IPC 对象。

image-20240406213437806

3 通过 id 访问 IPC 对象。

image-20240406213455072

image-20240406213505200

4 通过 id 控制 IPC 对象

image-20240406213523350

创建这三种 IPC 对象都要先获取 key 值, 然后根据 key 获取 id, 用到的函数如下:

image-20240406213535789

下面介绍消息队列:

​ 消息队列是类 unix 系统中一种数据传输的机制, 其他操作系统中也实现了这种机制, 可见这种通信机制在操作系统中有重要地位。
​ Linux 内核为每个消息队列对象维护一个 msqid_ds, 每个 msqid_ds 对应一个 id, 消息以链表形式存储,并且 msqid_ds 存放着这个链表的信息。

image-20240406213610202

消息队列的特点:
1.发出的消息以链表形式存储, 相当于一个列表, 进程可以根据 id 向对应的“列表” 增加和获取消息。
2.进程接收数据时可以按照类型从队列中获取数据。
消息队列的使用步骤:
1 创建 key;
2 msgget()通过 key 创建(或打开) 消息队列对象 id;
3 使用 msgsnd()/msgrcv()进行收发;
4 通过 msgctl()删除 ipc 对象
消息队列常用 API 如下:

msgget函数

通过 msgget()调用获取到 id 后即可使用消息队列访问 IPC 对象

image-20240406213709747

参数key需要调用ftok获取

msgsnd函数

image-20240406213717004

参数 const void *msgp的消息结构体:

struct msgbuf

{

  long mtype;     //消息的类型

  char mtext[128];   //消息的内容

};

参数 size_t msgsz指的是消息结构体的消息内容的实际内容值的大小

msgctl函数

image-20240406213725152

msgrcv函数

image-20240406213732171

参数 msgflg:

​ 0表示阻塞读取

​ IPC_NOWAIT表示非阻塞读取

参数 msgtyp:用于选择接收哪种类型的消息。

​ 如果值为0,则接收队列中的第一个消息;

​ 如果值大于0,则接收类型等于msgtyp的消息;

​ 如果值小于0,则接收类型小于或等于msgtyp绝对值的最低优先级消息。注意:msgtyp只能选择一种特定类型的消息进行接收。

​ 如果多个相同类型的消息在队列中,msgrcv函数仅接收其中一条消息,并且不保证按照发送顺序接收。

实验代码

​ a.c 向消息队列里面写

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

struct msgbuf
{
    long mtype;         //消息的类型
    char mtext[128];    //消息的内容
};

int main(int argc, const char *argv[])
{
    int msgid;
    key_t key;
    struct msgbuf msg;

    key = ftok("./a.c", 'a');
    msgid = msgget(key, 0666 | IPC_CREAT); // 创建消息队列
    if (msgid < 0)
    {
        printf("msgget is error\n");
        return -1;
    }
    printf("msgget is ok and msgid is %d\n", msgid);
    msg.mtype = 1;
    strncpy(msg.mtext, "hello", 5);
    msgsnd(msgid, &msg, strlen(msg.mtext), 0);
    return 0;
}

b.c 从消息队列里面读

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

struct msgbuf
{
    long mtype;         //消息的类型
    char mtext[128];    //消息的内容
};

int main(int argc, const char *argv[])
{
    int msgid;
    key_t key;
    struct msgbuf msg;

    key = ftok("./a.c", 'a');
    msgid = msgget(key, 0666 | IPC_CREAT); // 创建消息队列
    if (msgid < 0)
    {
        printf("msgget is error\n");
        return -1;
    }
    printf("msgget is ok and msgid is %d\n", msgid);
    msgrcv(msgid, &msg, sizeof(msg.mtext), 0, 0);
    printf("msg.mtype is %ld\n", msg.mtype);
    printf("msg.mtext is %s\n", msg.mtext);
    return 0;
}

在Ubuntu 上开一个终端, 编译运行 a 如下图所示:

image-20240407000055219

输入以下命令查看如下图所示:

image-20240407000102755

在 Ubuntu 再开一个终端, 编译运行 b 如下图所示, 成功从消息队列里面读到信息。

image-20240407000112683

信号量

​ 信号与信号量是不同的两种事物。

​ 为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题, 我们需要一种方法, 它可以通过生成并使用令牌来授权, 在任一时刻只能有一个执行线程访问代码的临界区域。

​ 临界区域是指执行数据更新的代码需要独占式地执行。 而信号量就可以提供这样的一种访问机制, 让一个临界区同一时间只有一个线程在访问它, 也就是说信号量是用来调协进程对共享资源的访问的。
​ 信号量是一个特殊的变量, 程序对其访问都是原子操作, 且只允许对它进行等待(即 P(信号变量))和发送(即 V(信号变量))信息操作。 最简单的信号量是只能取 0 和 1 的变量, 这也是信号量最常见的一种形式,叫做二进制信号量。 而可以取多个正整数的信号量被称为通用信号量。 这里主要讨论二进制信号量。
​ 由于信号量只能进行两种操作等待和发送信号, 即 P(sv)和 V(sv),他们的行为是这样的:
​ P(sv): 如果 sv 的值大于零, 就给它减 1; 如果它的值为零, 就挂起该进程的执行
​ V(sv): 如果有其他进程因等待 sv 而被挂起, 就让它恢复运行, 如果没有进程因等待 sv 而挂起, 就给它加 1。
​ 举个例子, 就是两个进程共享信号量 sv, 一旦其中一个进程执行了 P(sv)操作, 它将得到信号量, 并可以进入临界区, 使 sv 减 1。 而第二个进程将被阻止进入临界区, 因为当它试图执行 P(sv)时, sv 为 0, 它会被挂起以等待第一个进程离开临界区域并执行 V(sv)释放信号量, 这时第二个进程就可以恢复执行。
​ 信号灯也叫信号量, 它能够用来同步进程的动作, 不能传输数据。 它的应用场景就像红绿灯, 控制各进程使用共享资源的顺序。 Posix 无名信号灯用于线程同步, Posix 有名信号灯, System V 信号灯。 信号灯相当于一个值大于或等于 0 计数器, 信号灯值大于 0, 进程就可以申请资源, 信号灯值-1, 如果信号灯值为0, 一个进程还想对它进行-1, 那么这个进程就会阻塞, 直到信号灯值大于 1。
​ 使用 System V 信号灯的步骤如下:
1 使用 semget()创建或打开一个信号灯集。
2 使用 semctl()初始化信号灯集, 。
3 使用 semop()操作信号灯值, 即进行 P/V 操作。
​ P 操作: 申请资源, 申清完后信号灯值-1;
​ V 操作: 释放资源, 释放资源后信号灯值+1;
​ Linux 提供了一组精心设计的信号量接口来对信号进行操作, 它们不只是针对二进制信号量, 下面将会对这些函数进行介绍, 但请注意, 这些函数都是用来对成组的信号量值进行操作的。 它们声明在头文件sys/sem.h 中。

semget函数

image-20240407000437197

参数 key: 通过 ftok 函数获取

参数 int semflg:

​ 这个参数控制信号量集的访问权限和状态。它是一系列标志的组合,可以用来设置信号量集的访问权限。这些权限使用与文件系统相同的方式(例如,0644表示所有者具有读写权限,而组和其他用户具有只读权限)。

semflg还可以包含两个额外的控制标志:

​ IPC_CREAT: 如果指定的键值不存在,则创建一个新的信号量集。如果信号量集已存在,这个标志没有效果。
​ IPC_EXCL: 与IPC_CREAT一起使用时,如果信号量集已存在,则semget调用失败。这用于确保调用者创建的是一个全新的信号量集。

semctl函数

image-20240407000444757

image-20240407000453550

参数 semnum: 要修改的信号量的编号

参数 arg:

union semun

{

int val;

struct semid_ds *buf;

unsigned short *array;

struct seminfo *__buf;

};

semop函数

image-20240407000503213

结构体 sembuf

struct sembuf

{

unsigned short sem_num; // 要操作的信号量的编号

short sem_op; // P/V 操作, 1 为 V 操作, 释放资源。 -1 为 P操作, 分配资源。 0 为等待, 直到信号量的值变成 0

short sem_flg; // 0 表示阻塞, IPC_NOWAIT 表示非阻塞

};

实验代码:

指定哪个进程运行,可以使用进程间通信的知识, 或者使用信号量, 这里以使用信号量为例:

posted @ 2024-04-10 19:56  爱吃冰激凌的黄某某  阅读(7)  评论(0编辑  收藏  举报