【Linux进程及通信机制实验方案——LED作业与按键作业交互】

Linux进程及通信机制实验方案------LED作业与按键作业交互

一、基础知识

进程相关

1.main

无参数形式

c 复制代码
int main(void);

c 复制代码
int main();

这两种形式都表示main函数不接收命令行参数。在C99标准之前,main函数没有参数的形式被写为int main(),这在某些情况下可能导致与int main(void)行为不完全相同的问题,因为int main()在老式的C语言标准中不明确指出函数是否接受参数。从C99标准开始,推荐使用int main(void)明确指明main函数不接受任何参数,以提高代码的可读性和一致性。

有参数形式

c 复制代码
int main(int argc, char *argv[]);

 argc:传递给程序的命令行参数的数量

 argv:指向字符串数组的指针,存储了命令行参数

 argv[0]通常是程序的名称

 argv[1]到argv[argc-1]是实际的命令行参数

2.fork

(1)pid_t 表示进程 ID 的数据类型(本质是整数)

(2)fork()创建子进程,父进程返回子进程 PID,子进程返回 0,失败返回 - 1

(3)getpid()获取当前进程自己的 PID 的函数

(4)getppid()获取当前进程的父进程 PID 的函数

测试例程

c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    printf("海哥教老学员%d春暖花开!\n",getpid());
    pid_t pid =  fork();
    if (pid < 0)
    {
        // 创建新进程失败
        printf("新学员加入失败\n");
return 1;
    }else if (pid == 0){
        // 这里往下的代码都是新的子进程的
        printf("新学员%d加入成功,他是老学员%d推荐的\n",getpid(),getppid());
    }else{
        // 这里往下继续运行父进程
        printf("老学员%d继续深造,他推荐了%d\n",getpid(),pid);
    }
    return 0;
}


3.文件描述符的引用计数和close()

测试例程

c 复制代码
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h> 
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main(int argc, char const *argv[])
{
    // fork之前
    // 打开一个文件
    int fd = open("io.txt",O_CREAT | O_WRONLY | O_APPEND ,0644);
    if (fd == -1)
    {
        perror("open");
        exit(EXIT_FAILURE);
    }
    char buffer[1024];//缓冲区存放写出的数据
    pid_t pid = fork();
    if (pid < 0)
    {
        perror("fork");
        exit(EXIT_FAILURE);
    }else if (pid == 0){
        // 子进程代码
        strcpy(buffer,"这是子进程写入的数据!\n");
    }else {
        // 父进程代码
        sleep(1);
        strcpy(buffer,"这是父进程写入的数据!\n");
    }
    // 父子进程都要执行的代码
    ssize_t bytes_write = write(fd,buffer,strlen(buffer));
   
    if (bytes_write == -1)
    {
        perror("write");
        close(fd);
        exit(EXIT_FAILURE);
    }
    printf("写入数据成功\n");
    // 使用完毕之后关闭
    close(fd);
    if (pid == 0)
    {
        printf("子进程写入完毕,并释放文件描述符\n");
    }else{
        printf("父进程写入完毕,并释放文件描述符\n");
    }
    return 0;
}

通过open()以写入追加的模式打开了io.txt文件,如果这个文件不存在则创建。上述程序的逻辑是:打开io.txt文件,获得文件描述符后,执行fork()创建子进程。分别在父子进程中向文件追加写入,并在写入完成后关闭。为了区分父子进程的操作,在父进程中通过sleep()休眠1s。

"引用次数(引用计数)",核心是Linux 内核中用来统计 "有多少个文件描述符正指向同一个打开的文件结构体" 的数值,引用次数 = 这扇门当前有多少个 "把手"(文件描述符)能操控它。

• 引用次数 = 这扇门当前有多少个 "把手"(文件描述符)能操控它;

• 你代码中父进程open后,只有 1 个把手(父进程的fd),引用次数 = 1;

• fork创建子进程后,子进程复制了这个把手,现在有 2 个把手(父 fd + 子 fd),引用次数 = 2;

• 子进程close(fd) = 拆掉子进程的把手,引用次数减 1(变成 1);

• 父进程close(fd) = 拆掉父进程的把手,引用次数减 1(变成 0);

• 引用次数 = 0 时,这扇门(文件结构体)才会被内核 "拆掉"(释放文件资源)。

4.execve

创建erlou.c

c 复制代码
#include <stdio.h>
#include <stdlib.h> 
#include <unistd.h>

int main(int argc, char *argv[])
{
    if (argc < 2) {
        printf("参数不够,上不了二楼.\n");
        return 1; // 当没有传入参数时,应返回非零值表示错误
    }
    printf("我是%s %d,我跟海哥上二楼啦!\n", argv[1], getpid());
    return 0;
}

创建execve_test.c

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    /*exec系列函数  父进程跳转进入一个新进程
推荐使用execve
    char *__path: 需要执行程序的完整路径名
    char *const __argv[]: 指向字符串数组的指针 需要传入多个参数
        (1) 需要执行的程序命令(同*__path)
        (2) 执行程序需要传入的参数
        (3) 最后一个参数必须是NULL
    char *const __envp[]: 指向字符串数组的指针 需要传入多个环境变量参数
        (1) 环境变量参数 固定格式 key=value
        (2) 最后一个参数必须是NULL
    return: 成功就回不来了 下面的代码都没有意义
            失败返回-1
    int execve (const char *__path, char *const __argv[], char *const __envp[]) 
    */
    char *name = "banzhang";
    printf("我是%s %d,我现在在一楼\n",name,getpid());
    // 参数没填写够也能完成跳转,错误信息会在新程序中
    // char *argv[] = {"/home/atguigu/process_test/erlou",NULL};
    char *args[] = {"/home/atguigu/process_test/erlou",name,NULL};
    // 环境变量可以不传
    // char *envp[] = {NULL};
    char *envs[] = {"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin",NULL};
    int re = execve(argv[0],args,envs);
    if (re == -1)
    {
        printf("你没机会上二楼\n");
        return -1;
    }
    
    return 0;
}

操作系统内核(execve系统调用)

加载erlou程序到当前进程内存

执行erlou程序的_start函数(C运行时库提供)


_start函数做初始化(比如解析命令行参数、设置环境变量)


_start调用erlou.c的main(argc, argv)(把参数传递进去)

执行你写的main函数里的代码(打印"上二楼")

main返回后,_start调用exit()结束进程

5. execve+fork

同样的erlou.c

c 复制代码
#include <stdio.h>
#include <stdlib.h> 
#include <unistd.h>

int main(int argc, char *argv[])
{
    if (argc < 2) {
        printf("参数不够,上不了二楼.\n");
        return 1; // 当没有传入参数时,应返回非零值表示错误
    }
    printf("我是%s %d,我跟海哥上二楼啦!\n", argv[1], getpid());
    return 0;
}

fork_execve_test.c,

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
int main(int argc, char const *argv[])
{
    char *name="老学员";
    printf("%s%d在一楼精进\n",name,getpid());
    __pid_t pid = fork();
    if (pid == -1)
    {
        printf("邀请新学员失败!\n");
    }else if (pid == 0)
    {
        // 新学员在这里
        char *newName = "ergou";
        char *args[] = {"/home/atguigu/process_test/erlou",newName,NULL};
        char *envs[] = {"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin",NULL};
        int re = execve(argv[0],args,envs);
        if (re == -1)
        {
            printf("新学员上二楼失败\n");
            return 1;
        }
        
    }else{
        // 老学员在这里
        printf("老学员%d邀请完%d之后还是在一楼学习\n",getpid(),pid);
        
    }
    return 0;
}


6.waitpid

Linux中父进程除了可以启动子进程,还要负责回收子进程的状态。如果子进程结束后父进程没有正常回收,那么子进程就会变成一个僵尸进程------即程序执行完成,但是进程没有完全结束,其内核中PCB结构体(下文介绍)没有释放。在上面的例子中,父进程在子进程结束前就结束了,那么其子进程的回收工作就交给了父进程的父进程的父进程(省略若干父进程)。

wait()

wait () 是 waitpid () 的简化版,两者都是父进程用来 "回收" 子进程资源、获取子进程退出状态的工具,只是 waitpid () 功能更灵活。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
    pid_t pid = fork(); // 创建子进程
    if (pid == 0) {
        // 子进程逻辑:运行5秒后退出
        printf("子进程(PID:%d)运行中...\n", getpid());
        sleep(5);
        exit(10); // 退出码设为10
    } else if (pid > 0) {
        // 父进程逻辑:等待子进程退出
        int status;
        pid_t dead_pid = wait(&status); // 阻塞等待任意子进程退出
        printf("回收了子进程PID:%d\n", dead_pid);
        
        // 解析退出状态:判断是否正常退出,以及退出码
        if (WIFEXITED(status)) { // 宏:判断是否正常退出(比如调用exit/return)
            printf("子进程正常退出,退出码:%d\n", WEXITSTATUS(status)); // 提取退出码
        }
    }
    return 0;
}

子进程(PID:1234)运行中...

回收了子进程PID:1234

子进程正常退出,退出码:10

waitpid ()

• wait () 只能等 "任意子进程",waitpid () 可以指定等某个 / 某组子进程;

• wait () 只能 "阻塞等",waitpid () 可以 "非阻塞查"(问一下就走,不卡着)。

PS:记住

• 子进程中:led_pid 是 0(但子进程里我们不会调用 waitpid,因为 waitpid 是父进程用来等子进程的);

• 父进程中:led_pid 是子进程的真实 PID(比如 1234)------ 这才是我们在 waitpid(led_pid, &status, WNOHANG) 里用的变量值!

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
    pid_t led_pid = fork(); // 创建LED子进程
    if (led_pid == 0) {
        printf("LED子进程(PID:%d)运行...\n", getpid());
        sleep(3); // 模拟运行3秒
        exit(1); // 退出码1
    }

    pid_t key_pid = fork(); // 创建按键子进程
    if (key_pid == 0) {
        printf("按键子进程(PID:%d)运行...\n", getpid());
        sleep(5); // 模拟运行5秒
        exit(2); // 退出码2
    }
c 复制代码
   // 父进程:非阻塞监控两个子进程
    int status;
    while (1) {
        // 先查LED子进程
        pid_t ret = waitpid(led_pid, &status, WNOHANG); // 非阻塞
        if (ret == led_pid) { // LED子进程退出了
            printf("LED子进程退出,退出码:%d\n", WEXITSTATUS(status));
            led_pid = -1; // 标记已处理,下次不查了
        }

        // 再查按键子进程
        ret = waitpid(key_pid, &status, WNOHANG);
        if (ret == key_pid) { // 按键子进程退出了
            printf("按键子进程退出,退出码:%d\n", WEXITSTATUS(status));
            key_pid = -1; // 标记已处理
        }

        // 两个子进程都处理完了,退出循环
        if (led_pid == -1 && key_pid == -1) break;

        // 没子进程退出,父进程干点别的(比如打印日志)
        printf("父进程:两个子进程还在运行,我先忙别的...\n");
        sleep(1);
    }

    printf("父进程:所有子进程都回收完毕,退出\n");
    return 0;
}

LED子进程(PID:1234)运行...

按键子进程(PID:1235)运行...

父进程:两个子进程还在运行,我先忙别的...

父进程:两个子进程还在运行,我先忙别的...

父进程:两个子进程还在运行,我先忙别的...

LED子进程退出,退出码:1

父进程:两个子进程还在运行,我先忙别的...

父进程:两个子进程还在运行,我先忙别的...

按键子进程退出,退出码:2

父进程:所有子进程都回收完毕,退出

6.进程间通信

msgget():创建 / 获取消息队列(掌握 key_t 键值生成,比如 ftok() 或 IPC_PRIVATE);

• msgsnd():按键进程发送消息(比如把 "LED_ON""LED_OFF" 封装成消息结构体);

• msgrcv():LED 进程接收消息(按消息类型过滤,只处理自己关心的指令);

• msgctl():实验结束销毁消息队列(避免系统资源泄漏);

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/msg.h>
#include <sys/wait.h>
#include <unistd.h>

// 1. 定义消息结构体(必须以long mtype开头!)
// mtype:消息类型(>0),用于过滤消息;mtext:消息内容
struct msg_buf {
    long mtype;          // 消息类型,必须是long型
    char mtext[128];     // 消息内容,可自定义长度
};

int main() {
    // 2. 第一步:创建消息队列
    // key_t key:队列的唯一标识(用IPC_PRIVATE表示私有队列,仅父子进程可用)
    // 0666:队列权限(可读可写)
    // IPC_CREAT:如果队列不存在则创建,存在则获取
    int msgid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
    if (msgid == -1) {  // 错误判断:创建/获取失败
        perror("msgget failed");  // 打印错误原因
        exit(1);
    }
    printf("父进程:消息队列创建成功,msgid=%d\n", msgid);

    // 3. 创建子进程(模拟按键进程/LED进程)
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork failed");
        exit(1);
    }

    // ===== 子进程逻辑(生产者:发送消息)=====
    if (pid == 0) {
        struct msg_buf msg;
        // 设置消息类型(必须>0,比如1代表LED指令)
        msg.mtype = 1;
        // 填充消息内容(比如模拟按键按下:"LED_ON")
        strcpy(msg.mtext, "LED_ON");
        
        // 发送消息到队列
        // 参数:队列ID、消息结构体、消息内容长度、发送选项(0=阻塞)
        int ret = msgsnd(msgid, &msg, strlen(msg.mtext), 0);
        if (ret == -1) {
            perror("msgsnd failed");
            exit(1);
        }
        printf("子进程(PID:%d):发送消息成功 → 类型:%ld,内容:%s\n", 
               getpid(), msg.mtype, msg.mtext);
        
        // 子进程发送完消息后退出
        exit(0);
    }

    // ===== 父进程逻辑(消费者:接收消息)=====
    if (pid > 0) {
        // 先等子进程发送完消息(也可以不用等,msgsnd/msgrcv会阻塞)
        wait(NULL);

        struct msg_buf msg;
        // 接收消息:只接收类型为1的消息
        // 参数:队列ID、消息结构体、缓冲区长度、消息类型、接收选项(0=阻塞)
        int ret = msgrcv(msgid, &msg, sizeof(msg.mtext), 1, 0);
        if (ret == -1) {
            perror("msgrcv failed");
            exit(1);
        }
        printf("父进程(PID:%d):接收消息成功 → 类型:%ld,内容:%s\n", 
               getpid(), msg.mtype, msg.mtext);

        // 4. 销毁消息队列(避免资源泄漏)
        // IPC_RMID:删除队列;NULL:不需要获取队列属性
        ret = msgctl(msgid, IPC_RMID, NULL);
        if (ret == -1) {
            perror("msgctl failed");
            exit(1);
        }
        printf("父进程:消息队列销毁成功\n");
    }

    return 0;
}

父进程:消息队列创建成功,msgid=12345(msgid是系统分配的,每个人不一样)

子进程(PID:6789):发送消息成功 → 类型:1,内容:LED_ON

父进程(PID:6788):接收消息成功 → 类型:1,内容:LED_ON

父进程:消息队列销毁成功

LED相关

1.普适的 GPIO 引脚操作方法

1.使能模块(Enable Power/Clock)

• 操作目标:让 GPIO 模块从休眠状态被唤醒,获得电源和时钟信号。

• 原因:为了省电,芯片里的外设模块默认是关闭的,必须先通过 Power/Clock control 寄存器给目标 GPIO 组供电、提供时钟,它才能开始工作。

2.设置引脚模式(Pin Mode Selection)

• 操作目标:通过 io_mux(多路选择器)把引脚切换到 GPIO 功能。

• 原因:芯片上的很多引脚是 "复用" 的,比如一个引脚既可以当 GPIO,也可以当 UART 串口。你需要配置 io_mux 寄存器,明确告诉芯片这个引脚现在要作为 GPIO 使用。

3.设置方向(Direction: Input/Output)

• 操作目标:在 GPIO 模块内部,通过 gpio_dir_reg 寄存器设置引脚是输入还是输出。

• 原因:GPIO 既可以 "对外输出" 高低电平,也可以 "读取外部" 的电平信号,必须先确定它的工作方向。

4.读写数据(Data Read/Write)

• 如果是输出模式:通过 gpio_data_reg 寄存器写入 1 或 0,让引脚输出高电平或低电平。

• 如果是输入模式:通过 gpio_data_reg 寄存器读取当前引脚的电平状态(1 为高,0 为低),从而获取外部设备的信号。


c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

// 替换为验证通过的引脚编号:131
#define LED_PIN 131

// 封装:向指定文件写入内容(GPIO操作核心)
static int write_gpio_file(const char *file_path, const char *content)
{
    FILE *fp = fopen(file_path, "w");
    if (fp == NULL) {
        perror("fopen failed");
        return -1;
    }
    fwrite(content, 1, strlen(content), fp);
    fclose(fp);
    return 0;
}

int main(int argc, char *argv[])
{
    char pin_str[16] = {0};
    char gpio_path[64] = {0};
    
    // 1. 导出GPIO引脚
    snprintf(pin_str, sizeof(pin_str), "%d", LED_PIN);
    if (write_gpio_file("/sys/class/gpio/export", pin_str) != 0) {
        printf("导出引脚%d失败\n", LED_PIN);
        return -1;
    }
    usleep(100000); // 给系统100ms响应时间

    // 2. 设置GPIO为输出模式
    snprintf(gpio_path, sizeof(gpio_path), "/sys/class/gpio/gpio%d/direction", LED_PIN);
    if (write_gpio_file(gpio_path, "out") != 0) {
        printf("设置输出模式失败\n");
        return -1;
    }

    // 3. 控制LED闪烁3次(亮1秒,灭1秒)
    snprintf(gpio_path, sizeof(gpio_path), "/sys/class/gpio/gpio%d/value", LED_PIN);
    for (int i = 0; i < 3; i++) {
        // 点亮LED(低电平亮,写0)
        write_gpio_file(gpio_path, "0");
        printf("第%d次:LED亮\n", i+1);
        sleep(1);

        // 熄灭LED(高电平灭,写1)
        write_gpio_file(gpio_path, "1");
        printf("第%d次:LED灭\n", i+1);
        sleep(1);
    }

    // 4. 释放GPIO引脚
    write_gpio_file("/sys/class/gpio/unexport", pin_str);
    printf("测试完成,释放引脚%d\n", LED_PIN);

    return 0;
}

按键相关


极简的key_code_dump.c,只做一件事:打印/dev/input/event1的所有按键事件,找到按键编码:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <linux/input.h>

#define KEY_DEV "/dev/input/event1"

int main(void)
{
    int fd = open(KEY_DEV, O_RDONLY);
    if (fd < 0) {
        perror("打开按键设备失败");
        return -1;
    }
    printf("开始读取按键事件(按Ctrl+C退出)...\n");
    printf("格式:type=xxx, code=xxx, value=xxx\n");
    printf("说明:value=1=按下,value=0=松开,value=2=重复\n\n");

    struct input_event ev;
    while (read(fd, &ev, sizeof(ev)) == sizeof(ev)) {
        // 只打印按键相关事件(EV_KEY)
        if (ev.type == EV_KEY) {
            printf("type=%d, code=%d, value=%d\n", ev.type, ev.code, ev.value);
        }
    }

    close(fd);
    return 0;
}

可以确定一个是2一个是3

检测按键控制LED

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <linux/input.h>
#include <string.h>

#define LED_PIN 131
#define KEY_DEV "/dev/input/event1"
#define TARGET_KEY_CODE 3

// LED控制
void led_switch(int on) {
    char val_path[64];
    snprintf(val_path, sizeof(val_path), "/sys/class/gpio/gpio%d/value", LED_PIN);
    FILE *fp = fopen(val_path, "w");
    if (fp == NULL) return;
    fwrite(on ? "0" : "1", 1, 1, fp);
    fclose(fp);
}

int main(void) {
    // 初始化LED
    char pin_str[16];
    snprintf(pin_str, sizeof(pin_str), "%d", LED_PIN);
    
    FILE *fp = fopen("/sys/class/gpio/export", "w");
    if (fp != NULL) {
        fwrite(pin_str, 1, strlen(pin_str), fp);
        fclose(fp);
    }
    usleep(100000);
    
    char dir_path[64];
    snprintf(dir_path, sizeof(dir_path), "/sys/class/gpio/gpio%d/direction", LED_PIN);
    fp = fopen(dir_path, "w");
    if (fp != NULL) {
        fwrite("out", 1, 3, fp);
        fclose(fp);
    }

    // 按键控制核心
    int fd = open(KEY_DEV, O_RDONLY);
    if (fd < 0) return -1;
    
    struct input_event ev;
    while (read(fd, &ev, sizeof(ev)) == sizeof(ev)) {
        if (ev.type == EV_KEY && ev.code == TARGET_KEY_CODE && ev.value != 2) {
            led_switch(ev.value == 1);
        }
    }

    // 释放资源
    close(fd);
    fp = fopen("/sys/class/gpio/unexport", "w");
    if (fp != NULL) {
        fwrite(pin_str, 1, strlen(pin_str), fp);
        fclose(fp);
    }
    return 0;
}

二、具体实验相关

二、 具体实验相关

parent.c

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/msg.h>
#include <sys/wait.h>
#include <signal.h>

#define MSG_KEY 0x56789

pid_t pid_led = -1, pid_key = -1;
int msgid = -1;

/* 实验结束处理:Ctrl+C */
void sig_handler(int sig) {
    if (sig == SIGINT) {
        printf("\n[父进程] 收到结束信号,开始回收资源...\n");

        if (pid_led > 0) kill(pid_led, SIGTERM);
        if (pid_key > 0) kill(pid_key, SIGTERM);

        while (wait(NULL) > 0);   // 回收子进程

        if (msgid != -1) {
            msgctl(msgid, IPC_RMID, NULL);
            printf("[父进程] 消息队列已销毁\n");
        }

        printf("[父进程] 实验结束\n");
        exit(0);
    }
}

int main() {
    signal(SIGINT, sig_handler);

    /* 1. 创建消息队列 */
    msgid = msgget(MSG_KEY, IPC_CREAT | 0666);
    if (msgid == -1) {
        perror("msgget");
        exit(1);
    }
    printf("[父进程] 创建消息队列 ID=%d\n", msgid);

char msgid_str[16];
// 把msgid = 163841变成msgid_str = "163841"为了后面exec传参
    sprintf(msgid_str, "%d", msgid);

    /* 2. fork + exec LED 作业进程 */
    pid_led = fork();
    if (pid_led == 0) {
        execl("./led_process", "led_process", msgid_str, NULL);
        perror("exec led_process");
        exit(1);
    }

    /* 3. fork + exec 按键作业进程 */
    pid_key = fork();
    if (pid_key == 0) {
        execl("./key_process", "key_process", msgid_str, NULL);
        perror("exec key_process");
        exit(1);
    }

    printf("[父进程] LED进程 PID=%d,按键进程 PID=%d\n", pid_led, pid_key);

    /* 4. 实验监控 */
    while (1) {
        sleep(1);
    }
}

led_process.c

c 复制代码
在这里插入代码片
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/msg.h>
#include <string.h>
#include <signal.h>

#define LED_PIN 131

struct msgbuf {
    long mtype;
    char mtext[32];
};

typedef enum {
    LED_OFF,
    LED_ON,
    LED_BLINK
} led_mode_t;

volatile led_mode_t mode = LED_OFF;

/* GPIO 写电平 */
void led_write(int on) {
    char path[64];
    sprintf(path, "/sys/class/gpio/gpio%d/value", LED_PIN);
    FILE *fp = fopen(path, "w");
    if (!fp) return;
    fwrite(on ? "0" : "1", 1, 1, fp);
    fclose(fp);
}

/* GPIO 初始化 */
void led_init() {
    FILE *fp = fopen("/sys/class/gpio/export", "w");
    if (fp) {
        fprintf(fp, "%d", LED_PIN);
        fclose(fp);
    }
    usleep(100000);

    char dir[64];
    sprintf(dir, "/sys/class/gpio/gpio%d/direction", LED_PIN);
    fp = fopen(dir, "w");
    if (fp) {
        fwrite("out", 1, 3, fp);
        fclose(fp);
    }

    led_write(0);
}

/* 优雅退出 */
void sig_handler(int sig) {
    led_write(0);
    exit(0);
}

int main(int argc, char *argv[]) {
    if (argc < 2) exit(1);
    int msgid = atoi(argv[1]);
//对应execl("./led_process", "led_process", msgid_str, NULL);
//相当于./led_process 163841
//argv[0] = "led_process",argv[1] = "163841"
    signal(SIGTERM, sig_handler);
    signal(SIGINT, sig_handler);

    led_init();
    printf("[LED进程] 初始化完成\n");

    struct msgbuf msg;

    while (1) {
        /* 非阻塞接收消息 */
        if (msgrcv(msgid, &msg, sizeof(msg.mtext), 1, IPC_NOWAIT) > 0) {
            if (strcmp(msg.mtext, "LED_ON") == 0)
                mode = LED_ON;
            else if (strcmp(msg.mtext, "LED_OFF") == 0)
                mode = LED_OFF;
            else if (strcmp(msg.mtext, "LED_BLINK") == 0)
                mode = LED_BLINK;
        }

        /* 根据状态执行 */
        switch (mode) {
            case LED_ON:
                led_write(1);
                usleep(200000);
                break;
            case LED_OFF:
                led_write(0);
                usleep(200000);
                break;
            case LED_BLINK:
                led_write(1);
                usleep(500000);
                led_write(0);
                usleep(500000);
                break;
        }
    }
}

key_process.c

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/msg.h>
#include <linux/input.h>
#include <fcntl.h>
#include <string.h>
#include <time.h>

#define KEY_DEV "/dev/input/event1"
#define TARGET_KEY_CODE 3

struct msgbuf {
    long mtype;
    char mtext[32];
};

int main(int argc, char *argv[]) {
    if (argc < 2) exit(1);
    int msgid = atoi(argv[1]);

    int fd = open(KEY_DEV, O_RDONLY);
    if (fd < 0) {
        perror("open key device");
        exit(1);
    }

    struct msgbuf msg;
    msg.mtype = 1;

    struct input_event ev;
    time_t press_time = 0;
    time_t last_release = 0;
    int click_count = 0;

    printf("[按键进程] 开始检测按键\n");

    while (read(fd, &ev, sizeof(ev)) == sizeof(ev)) {
        if (ev.type == EV_KEY && ev.code == TARGET_KEY_CODE) {

            if (ev.value == 1) {           // 按下
                press_time = time(NULL);
            }
            else if (ev.value == 0) {      // 松开
                time_t now = time(NULL);
                int dur = now - press_time;

                if (now - last_release < 1)
                    click_count++;
                else
                    click_count = 1;

                last_release = now;

                if (click_count == 2) {
                    strcpy(msg.mtext, "LED_OFF");
                    click_count = 0;
                } else {
                    if (dur < 1)
                        strcpy(msg.mtext, "LED_ON");
                    else
                        strcpy(msg.mtext, "LED_BLINK");
                }

                msgsnd(msgid, &msg, sizeof(msg.mtext), 0);
                printf("[按键进程] 发送指令:%s\n", msg.mtext);
            }
        }
    }

    close(fd);
}

整体梳理

一、按键输入事件产生流程(硬件 → 用户态)

1️⃣ 用户按下实体按键

2️⃣ 按键 GPIO 引脚电平变化,触发硬件中断

3️⃣ GPIO / 按键驱动程序响应中断

4️⃣ 驱动将按键事件封装为 input_event 结构

5️⃣ Linux 输入子系统将事件写入 /dev/input/event1 设备队列

6️⃣ 按键作业进程调用 read() 阻塞等待事件

7️⃣ 有事件到来,read() 立即返回,struct input_event ev 被内核填充


二、按键语义识别流程(短按 / 长按 / 双击)

1️⃣ 按键进程检测到 EV_KEY + TARGET_KEY_CODE 事件

2️⃣ 当 ev.value == 1 时,记录按键按下时间 press_time

3️⃣ 当 ev.value == 0 时,记录松开时间并计算按下持续时间

4️⃣ 根据持续时间判断短按(<1s)或长按(≥1s)

5️⃣ 结合前一次松开时间,判断是否构成双击操作


三、进程间通信流程(按键进程 → 消息队列)

1️⃣ 按键作业进程根据按键操作类型生成控制指令字符串

2️⃣ 将指令填入 msgbuf.mtext,设置消息类型 mtype = 1

3️⃣ 调用 msgsnd() 将消息发送到 System V 消息队列

4️⃣ 消息暂存于内核消息队列中,等待 LED 作业进程读取


四、LED 控制流程(消息队列 → GPIO)

1️⃣ LED 作业进程通过 msgrcv() 非阻塞读取消息队列

2️⃣ 若读取到消息,则解析 msg.mtext 中的控制指令

3️⃣ 根据指令更新 LED 当前工作模式(亮 / 灭 / 闪烁)

4️⃣ LED 作业进程根据当前模式循环控制 GPIO 电平

5️⃣ 通过 /sys/class/gpio/gpio131/value 文件实时驱动 LED 状态变化


五、实验结束与资源回收流程(父进程)

1️⃣ 用户按下 Ctrl + C,父进程收到 SIGINT 信号

2️⃣ 父进程向 LED 和按键作业进程发送 SIGTERM 信号

3️⃣ 子进程执行信号处理函数,安全退出并关闭设备

4️⃣ 父进程调用 msgctl(..., IPC_RMID) 销毁消息队列

5️⃣ 父进程调用 wait() 回收子进程资源,实验结束

相关推荐
Whoami!2 小时前
⓬⁄₆ ⟦ OSCP ⬖ 研记 ⟧ Linux权限提升 ➱ 从“守护进程”和“网络流量”中捕获敏感信息
linux·网络安全·信息安全·权限提升
星空22232 小时前
【HarmonyOS】HarmonyOS React Native实战:手势交互配置优化
react native·交互·harmonyos
郝学胜-神的一滴3 小时前
深入理解TCP连接的优雅关闭:半关闭状态与四次挥手的艺术
linux·服务器·开发语言·网络·tcp/ip·程序人生
CCPC不拿奖不改名11 小时前
虚拟机基础:在VMware WorkStation上安装Linux为容器化部署打基础
linux·运维·服务器·人工智能·milvus·知识库搭建·容器化部署
一只自律的鸡13 小时前
【Linux系统编程】文件IO 函数篇
linux·linux系统编程
诚思报告YH14 小时前
生物制剂与生物类似药市场洞察:2026-2032年复合增长率(CAGR)为8.1%
大数据·人工智能·microsoft
dinga1985102614 小时前
linux上redis升级
linux·运维·redis
hzc098765432114 小时前
Linux系统下安装配置 Nginx 超详细图文教程_linux安装nginx
linux·服务器·nginx
RisunJan15 小时前
Linux命令-ltrace(用来跟踪进程调用库函数的情况)
linux·运维·服务器