【linux】Linux内核和应用层常见的通信方式及举例整理

发布于:2024-11-28 ⋅ 阅读:(15) ⋅ 点赞:(0)

Linux内核和应用层常见的通信方式

系统调用(System Calls)

        应用程序通过系统调用与内核进行交互。这是最基本的通信方式,应用程序可以通过系统调用请求内核提供的服务,如文件操作、进程控制等。

proc文件系统

   /proc文件系统提供了一种从用户空间访问内核数据结构的方式。应用程序可以通过读取和写入/proc中的文件与内核通信。/proc文件系统是Linux内核提供的一种虚拟文件系统,用于向用户空间提供内核和进程的信息。它并不存储在磁盘上,而是由内核在内存中动态生成的。通过/proc文件系统,用户和应用程序可以访问内核数据结构和系统信息。

特性

动态生成/proc中的文件和目录是动态生成的,反映了系统的当前状态。

只读和可写:大多数/proc文件是只读的,但有些文件可以通过写入来修改内核参数。

进程信息:每个正在运行的进程在/proc中都有一个对应的目录,目录名为进程的PID(进程标识符)。

常见用途

系统信息

/proc/cpuinfo:提供CPU的信息,如型号、核心数、频率等。

/proc/meminfo:提供内存使用情况的信息。

/proc/uptime:系统启动后的运行时间。

进程信息

/proc/[pid]/cmdline:进程的启动命令行。

/proc/[pid]/status:进程的状态信息。

/proc/[pid]/fd:进程打开的文件描述符。

内核参数

/proc/sys目录下的文件可以用于查看和修改内核参数。例如,/proc/sys/net/ipv4/ip_forward用于控制IP转发功能。

设备和驱动信息

/proc/devices:显示当前已注册的字符设备和块设备。

/proc/interrupts:显示中断请求(IRQ)的使用情况。

网络信息

/proc/net/dev:显示网络接口的统计信息。

/proc/net/route:显示路由表信息。

编写内核模块/proc/helloworld

编写内核模块代码

创建一个名为proc_example.c的文件,并添加以下代码:

#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/uaccess.h> // for copy_to_user

#define PROC_NAME "helloworld"
#define MESSAGE "Hello, World!\n"

ssize_t proc_read(struct file *file, char __user *usr_buf, size_t count, loff_t *pos);

static struct proc_ops proc_ops = {
    .proc_read = proc_read,
};

static int __init proc_init(void) {
    proc_create(PROC_NAME, 0666, NULL, &proc_ops);
    printk(KERN_INFO "/proc/%s created\n", PROC_NAME);
    return 0;
}

static void __exit proc_exit(void) {
    remove_proc_entry(PROC_NAME, NULL);
    printk(KERN_INFO "/proc/%s removed\n", PROC_NAME);
}

ssize_t proc_read(struct file *file, char __user *usr_buf, size_t count, loff_t *pos) {
    int rv = 0;
    char buffer[128];
    static int completed = 0;

    if (completed) {
        completed = 0;
        return 0;
    }

    completed = 1;
    rv = sprintf(buffer, MESSAGE);

    // Copy data to user space
    if (copy_to_user(usr_buf, buffer, rv)) {
        rv = -EFAULT;
    }

    return rv;
}

module_init(proc_init);
module_exit(proc_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Hello World Proc Module");
MODULE_AUTHOR("Your Name");
编译内核模块

创建一个Makefile来编译这个模块:

obj-m += proc_example.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

在终端中运行以下命令来编译模块:

make
加载和测试模块

加载模块:

sudo insmod proc_example.ko

查看/proc/helloworld文件:

cat /proc/helloworld

您应该看到输出:

Hello, World!

卸载模块:

sudo rmmod proc_example

说明

  • proc_create:用于创建一个新的/proc文件。
  • proc_read:定义了读取/proc文件时的行为。在这个例子中,它返回一个简单的字符串。
  • copy_to_user:用于将数据从内核空间复制到用户空间。
  • module_init/module_exit:定义模块加载和卸载时的初始化和清理函数。

sysfs文件系统

        类似于/proc/sys文件系统用于导出内核对象的信息,特别是设备和驱动程序相关的信息。

编写sysfs文件系统例子

       创建一个名为/sys/kernel/my_sysfs_example的文件,当读取该文件时,将返回一个简单的消息,并允许通过写入来修改该消息。

编写内核模块代码

创建一个名为sysfs_example.c的文件,并添加以下代码:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/kobject.h>
#include <linux/sysfs.h>
#include <linux/string.h>

static struct kobject *example_kobject;
static char example_str[256] = "Hello, World!\n";

static ssize_t example_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) {
    return sprintf(buf, "%s", example_str);
}

static ssize_t example_store(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count) {
    snprintf(example_str, sizeof(example_str), "%s", buf);
    return count;
}

static struct kobj_attribute example_attribute = __ATTR(my_sysfs_example, 0664, example_show, example_store);

static int __init sysfs_example_init(void) {
    int error = 0;

    example_kobject = kobject_create_and_add("kernel", kernel_kobj);
    if (!example_kobject)
        return -ENOMEM;

    error = sysfs_create_file(example_kobject, &example_attribute.attr);
    if (error) {
        kobject_put(example_kobject);
    }

    return error;
}

static void __exit sysfs_example_exit(void) {
    kobject_put(example_kobject);
}

module_init(sysfs_example_init);
module_exit(sysfs_example_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Sysfs Example Module");
MODULE_AUTHOR("Your Name");
编译内核模块

创建一个Makefile来编译这个模块:

obj-m += sysfs_example.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

在终端中运行以下命令来编译模块:

make
加载和测试模块

加载模块:

sudo insmod sysfs_example.ko

查看/sys/kernel/my_sysfs_example文件:

cat /sys/kernel/my_sysfs_example

您应该看到输出:

Hello, World!

修改/sys/kernel/my_sysfs_example文件:

echo "New Message" | sudo tee /sys/kernel/my_sysfs_example

再次查看文件:

cat /sys/kernel/my_sysfs_example

您应该看到输出:

New Message

卸载模块:

sudo rmmod sysfs_example

说明

  • kobject_create_and_add:用于创建一个新的kobject并将其添加到sysfs中。
  • sysfs_create_file:用于在sysfs中创建一个文件。
  • example_show/example_store:定义了读取和写入sysfs文件时的行为。
  • module_init/module_exit:定义模块加载和卸载时的初始化和清理函数。

Netlink套接字

       Netlink是一种用于在Linux内核和用户空间进程之间进行通信的套接字机制。它广泛用于网络子系统,但也可以用于其他类型的内核与用户空间的通信。

Netlink示例

使用Netlink进行简单的消息传递

内核模块部分

首先,我们编写一个内核模块来处理来自用户空间的Netlink消息。

创建一个名为netlink_example.c的文件,并添加以下代码:

#include <linux/module.h>
#include <linux/netlink.h>
#include <linux/skbuff.h>
#include <net/sock.h>

#define NETLINK_USER 31

struct sock *nl_sk = NULL;

static void nl_recv_msg(struct sk_buff *skb) {
    struct nlmsghdr *nlh;
    int pid;
    struct sk_buff *skb_out;
    int msg_size;
    char *msg = "Hello from kernel";
    int res;

    nlh = (struct nlmsghdr *)skb->data;
    printk(KERN_INFO "Netlink received msg payload: %s\n", (char *)nlmsg_data(nlh));
    pid = nlh->nlmsg_pid; /*pid of sending process */

    msg_size = strlen(msg);

    skb_out = nlmsg_new(msg_size, 0);
    if (!skb_out) {
        printk(KERN_ERR "Failed to allocate new skb\n");
        return;
    }

    nlh = nlmsg_put(skb_out, 0, 0, NLMSG_DONE, msg_size, 0);
    NETLINK_CB(skb_out).dst_group = 0; /* not in mcast group */
    strncpy(nlmsg_data(nlh), msg, msg_size);

    res = nlmsg_unicast(nl_sk, skb_out, pid);
    if (res < 0)
        printk(KERN_INFO "Error while sending back to user\n");
}

static int __init netlink_init(void) {
    struct netlink_kernel_cfg cfg = {
        .input = nl_recv_msg,
    };

    nl_sk = netlink_kernel_create(&init_net, NETLINK_USER, &cfg);
    if (!nl_sk) {
        printk(KERN_ALERT "Error creating socket.\n");
        return -10;
    }

    printk(KERN_INFO "Netlink example module inserted.\n");
    return 0;
}

static void __exit netlink_exit(void) {
    netlink_kernel_release(nl_sk);
    printk(KERN_INFO "Netlink example module removed.\n");
}

module_init(netlink_init);
module_exit(netlink_exit);

MODULE_LICENSE("GPL");
用户空间程序部分

接下来,我们编写一个用户空间程序来与内核模块通信。

创建一个名为netlink_user.c的文件,并添加以下代码:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <linux/netlink.h>

#define NETLINK_USER 31
#define MAX_PAYLOAD 1024 /* maximum payload size*/

int main() {
    struct sockaddr_nl src_addr, dest_addr;
    struct nlmsghdr *nlh = NULL;
    struct iovec iov;
    int sock_fd;
    struct msghdr msg;

    sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_USER);
    if (sock_fd < 0) {
        return -1;
    }

    memset(&src_addr, 0, sizeof(src_addr));
    src_addr.nl_family = AF_NETLINK;
    src_addr.nl_pid = getpid(); /* self pid */

    bind(sock_fd, (struct sockaddr *)&src_addr, sizeof(src_addr));

    memset(&dest_addr, 0, sizeof(dest_addr));
    dest_addr.nl_family = AF_NETLINK;
    dest_addr.nl_pid = 0; /* For Linux Kernel */
    dest_addr.nl_groups = 0; /* unicast */

    nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
    memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
    nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
    nlh->nlmsg_pid = getpid();
    nlh->nlmsg_flags = 0;

    strcpy(NLMSG_DATA(nlh), "Hello from user space");

    iov.iov_base = (void *)nlh;
    iov.iov_len = nlh->nlmsg_len;
    msg.msg_name = (void *)&dest_addr;
    msg.msg_namelen = sizeof(dest_addr);
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;

    sendmsg(sock_fd, &msg, 0);

    /* Read message from kernel */
    recvmsg(sock_fd, &msg, 0);
    printf("Received message payload: %s\n", (char *)NLMSG_DATA(nlh));

    close(sock_fd);
    return 0;
}
编译和运行
  1. 编译内核模块

    创建一个Makefile用于编译内核模块:

    obj-m += netlink_example.o
    
    all:
    	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
    
    clean:
    	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

    然后在终端中运行:

    make
  2. 编译用户空间程序

    使用以下命令编译用户空间程序:

    gcc -o netlink_user netlink_user.c
  3. 加载内核模块

    使用以下命令加载内核模块:

    sudo insmod netlink_example.ko
  4. 运行用户空间程序

    运行用户空间程序:

    ./netlink_user

    您应该看到输出类似于:

    Received message payload: Hello from kernel
  5. 卸载内核模块

    使用以下命令卸载内核模块:

    sudo rmmod netlink_example

说明

  • Netlink Socket:在内核和用户空间之间创建通信通道。
  • nl_recv_msg:内核模块中的回调函数,用于处理来自用户空间的消息。
  • sendmsg/recvmsg:用户空间程序使用这些函数发送和接收Netlink消息。

ioctl

    ioctl系统调用允许应用程序对设备进行控制操作,通常用于设备驱动程序与用户空间的交互。ioctl(输入输出控制)是一个强大的系统调用,用于在用户空间和内核空间之间传递控制命令。它通常用于设备驱动程序,以便用户空间程序可以向设备发送命令或从设备获取状态信息。

使用ioctl控制字符设备

内核模块部分

首先,我们编写一个内核模块来实现一个简单的字符设备,并通过ioctl接口与用户空间交互。

创建一个名为ioctl_example.c的文件,并添加以下代码:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/cdev.h>
#include <linux/device.h>

#define DEVICE_NAME "ioctl_example"
#define CLASS_NAME "ioctl_class"
#define IOCTL_EXAMPLE _IOR('a', 1, int32_t *)

static int majorNumber;
static struct class* ioctlClass = NULL;
static struct device* ioctlDevice = NULL;
static int32_t value = 0;

static int dev_open(struct inode *inodep, struct file *filep) {
    printk(KERN_INFO "ioctl_example: Device opened\n");
    return 0;
}

static int dev_release(struct inode *inodep, struct file *filep) {
    printk(KERN_INFO "ioctl_example: Device closed\n");
    return 0;
}

static long dev_ioctl(struct file *filep, unsigned int cmd, unsigned long arg) {
    switch (cmd) {
        case IOCTL_EXAMPLE:
            if (copy_to_user((int32_t *)arg, &value, sizeof(value))) {
                return -EACCES;
            }
            printk(KERN_INFO "ioctl_example: Sent value %d to user\n", value);
            break;
        default:
            return -EINVAL;
    }
    return 0;
}

static struct file_operations fops = {
    .open = dev_open,
    .release = dev_release,
    .unlocked_ioctl = dev_ioctl,
};

static int __init ioctl_example_init(void) {
    majorNumber = register_chrdev(0, DEVICE_NAME, &fops);
    if (majorNumber < 0) {
        printk(KERN_ALERT "ioctl_example failed to register a major number\n");
        return majorNumber;
    }
    printk(KERN_INFO "ioctl_example: registered correctly with major number %d\n", majorNumber);

    ioctlClass = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(ioctlClass)) {
        unregister_chrdev(majorNumber, DEVICE_NAME);
        printk(KERN_ALERT "Failed to register device class\n");
        return PTR_ERR(ioctlClass);
    }
    printk(KERN_INFO "ioctl_example: device class registered correctly\n");

    ioctlDevice = device_create(ioctlClass, NULL, MKDEV(majorNumber, 0), NULL, DEVICE_NAME);
    if (IS_ERR(ioctlDevice)) {
        class_destroy(ioctlClass);
        unregister_chrdev(majorNumber, DEVICE_NAME);
        printk(KERN_ALERT "Failed to create the device\n");
        return PTR_ERR(ioctlDevice);
    }
    printk(KERN_INFO "ioctl_example: device class created correctly\n");
    return 0;
}

static void __exit ioctl_example_exit(void) {
    device_destroy(ioctlClass, MKDEV(majorNumber, 0));
    class_unregister(ioctlClass);
    class_destroy(ioctlClass);
    unregister_chrdev(majorNumber, DEVICE_NAME);
    printk(KERN_INFO "ioctl_example: Goodbye from the LKM!\n");
}

module_init(ioctl_example_init);
module_exit(ioctl_example_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("A simple Linux char driver with ioctl");
MODULE_AUTHOR("Your Name");
用户空间程序部分

接下来,我们编写一个用户空间程序来与内核模块通信。

创建一个名为ioctl_user.c的文件,并添加以下代码:

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

#define DEVICE_FILE_NAME "/dev/ioctl_example"
#define IOCTL_EXAMPLE _IOR('a', 1, int32_t *)

int main() {
    int fd;
    int32_t value = 0;

    fd = open(DEVICE_FILE_NAME, O_RDWR);
    if (fd < 0) {
        perror("Failed to open the device...");
        return errno;
    }

    if (ioctl(fd, IOCTL_EXAMPLE, &value) == -1) {
        perror("ioctl failed");
        close(fd);
        return errno;
    }

    printf("Received value from kernel: %d\n", value);

    close(fd);
    return 0;
}
编译和运行
  1. 编译内核模块

    创建一个Makefile用于编译内核模块:

    obj-m += ioctl_example.o
    
    all:
    	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
    
    clean:
    	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

    然后在终端中运行:

    make
  2. 编译用户空间程序

    使用以下命令编译用户空间程序:

    gcc -o ioctl_user ioctl_user.c
  3. 加载内核模块

    使用以下命令加载内核模块:

    sudo insmod ioctl_example.ko

    创建设备节点:

    sudo mknod /dev/ioctl_example c [major_number] 0

    请将[major_number]替换为加载模块时输出的主设备号。

  4. 运行用户空间程序

    运行用户空间程序:

    ./ioctl_user

    您应该看到输出类似于:

    Received value from kernel: 0
  5. 卸载内核模块

    使用以下命令卸载内核模块:

    sudo rmmod ioctl_example

    删除设备节点:

    sudo rm /dev/ioctl_example

说明

  • ioctl:在内核模块中实现了unlocked_ioctl函数,用于处理来自用户空间的ioctl请求。
  • _IOR:定义了一个ioctl命令,用于从内核读取数据。
  • copy_to_user:用于将数据从内核空间复制到用户空间。

字符设备和块设备接口

        应用程序可以通过文件操作接口(如openreadwrite)与字符设备和块设备进行通信。在Linux中,字符设备和块设备是两种基本的设备接口类型。字符设备允许按字节流的方式进行数据传输,而块设备则以固定大小的块进行数据传输。

创建一个简单的字符设备驱动程序

内核模块部分

我们将编写一个简单的字符设备驱动程序,允许用户空间程序通过/dev接口与其交互。

创建一个名为char_device_example.c的文件,并添加以下代码:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/cdev.h>
#include <linux/device.h>

#define DEVICE_NAME "char_example"
#define CLASS_NAME "char_class"

static int majorNumber;
static char message[256] = {0};
static short messageSize;
static struct class* charClass = NULL;
static struct device* charDevice = NULL;
static struct cdev charCdev;

static int dev_open(struct inode *inodep, struct file *filep) {
    printk(KERN_INFO "char_example: Device opened\n");
    return 0;
}

static int dev_release(struct inode *inodep, struct file *filep) {
    printk(KERN_INFO "char_example: Device closed\n");
    return 0;
}

static ssize_t dev_read(struct file *filep, char *buffer, size_t len, loff_t *offset) {
    int error_count = 0;
    error_count = copy_to_user(buffer, message, messageSize);

    if (error_count == 0) {
        printk(KERN_INFO "char_example: Sent %d characters to the user\n", messageSize);
        return (messageSize = 0); // clear the position to the start
    } else {
        printk(KERN_INFO "char_example: Failed to send %d characters to the user\n", error_count);
        return -EFAULT; // Failed -- return a bad address message (i.e. -14)
    }
}

static ssize_t dev_write(struct file *filep, const char *buffer, size_t len, loff_t *offset) {
    snprintf(message, sizeof(message), "%s", buffer);
    messageSize = strlen(message);
    printk(KERN_INFO "char_example: Received %zu characters from the user\n", len);
    return len;
}

static struct file_operations fops = {
    .open = dev_open,
    .read = dev_read,
    .write = dev_write,
    .release = dev_release,
};

static int __init char_example_init(void) {
    majorNumber = register_chrdev(0, DEVICE_NAME, &fops);
    if (majorNumber < 0) {
        printk(KERN_ALERT "char_example failed to register a major number\n");
        return majorNumber;
    }
    printk(KERN_INFO "char_example: registered correctly with major number %d\n", majorNumber);

    charClass = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(charClass)) {
        unregister_chrdev(majorNumber, DEVICE_NAME);
        printk(KERN_ALERT "Failed to register device class\n");
        return PTR_ERR(charClass);
    }
    printk(KERN_INFO "char_example: device class registered correctly\n");

    charDevice = device_create(charClass, NULL, MKDEV(majorNumber, 0), NULL, DEVICE_NAME);
    if (IS_ERR(charDevice)) {
        class_destroy(charClass);
        unregister_chrdev(majorNumber, DEVICE_NAME);
        printk(KERN_ALERT "Failed to create the device\n");
        return PTR_ERR(charDevice);
    }
    printk(KERN_INFO "char_example: device class created correctly\n");
    return 0;
}

static void __exit char_example_exit(void) {
    device_destroy(charClass, MKDEV(majorNumber, 0));
    class_unregister(charClass);
    class_destroy(charClass);
    unregister_chrdev(majorNumber, DEVICE_NAME);
    printk(KERN_INFO "char_example: Goodbye from the LKM!\n");
}

module_init(char_example_init);
module_exit(char_example_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("A simple Linux char driver");
MODULE_AUTHOR("Your Name");
编译和加载内核模块
  1. 创建Makefile

    创建一个Makefile用于编译内核模块:

    obj-m += char_device_example.o
    
    all:
    	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
    
    clean:
    	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
  2. 编译模块

    在终端中运行以下命令来编译模块:

    make
  3. 加载模块

    使用以下命令加载内核模块:

    sudo insmod char_device_example.ko
  4. 创建设备节点

    使用以下命令创建设备节点:

    sudo mknod /dev/char_example c [major_number] 0

    请将[major_number]替换为加载模块时输出的主设备号。

用户空间测试

您可以使用echocat命令与字符设备进行交互:

  1. 写入数据到设备

    echo "Hello, Kernel!" | sudo tee /dev/char_example
  2. 从设备读取数据

    cat /dev/char_example

    您应该看到输出:

    Hello, Kernel!
卸载模块
  1. 卸载模块

    使用以下命令卸载内核模块:

    sudo rmmod char_device_example
  2. 删除设备节点

    使用以下命令删除设备节点:

    sudo rm /dev/char_example

说明 

  • register_chrdev:注册一个字符设备并获取一个主设备号。
  • file_operations:定义设备的操作函数,包括打开、读取、写入和释放。
  • copy_to_user/copy_from_user:用于在内核空间和用户空间之间复制数据。

创建一个简单的块设备驱动程序

内核模块部分

创建一个名为block_device_example.c的文件,并添加以下代码:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/blkdev.h>
#include <linux/genhd.h>
#include <linux/errno.h>
#include <linux/vmalloc.h>
#include <linux/hdreg.h>

#define DEVICE_NAME "block_example"
#define DEVICE_SECTORS 1024
#define SECTOR_SIZE 512

static int major_num = 0;
static struct request_queue *queue;
static struct gendisk *gd;
static u8 *device_data;

static void block_example_request(struct request_queue *q) {
    struct request *req;
    while ((req = blk_fetch_request(q)) != NULL) {
        struct bio_vec bvec;
        struct req_iterator iter;
        sector_t sector = blk_rq_pos(req);
        unsigned int sector_offset = 0;
        unsigned int sectors = blk_rq_sectors(req);
        u8 *buffer;

        rq_for_each_segment(bvec, req, iter) {
            buffer = page_address(bvec.bv_page) + bvec.bv_offset;
            if (rq_data_dir(req) == WRITE) {
                memcpy(device_data + (sector + sector_offset) * SECTOR_SIZE, buffer, bvec.bv_len);
            } else {
                memcpy(buffer, device_data + (sector + sector_offset) * SECTOR_SIZE, bvec.bv_len);
            }
            sector_offset += bvec.bv_len / SECTOR_SIZE;
        }
        __blk_end_request_all(req, 0);
    }
}

static int block_example_open(struct block_device *bdev, fmode_t mode) {
    return 0;
}

static void block_example_release(struct gendisk *disk, fmode_t mode) {
}

static int block_example_getgeo(struct block_device *bdev, struct hd_geometry *geo) {
    geo->heads = 1;
    geo->sectors = 32;
    geo->cylinders = DEVICE_SECTORS / 32;
    geo->start = 0;
    return 0;
}

static struct block_device_operations block_example_ops = {
    .owner = THIS_MODULE,
    .open = block_example_open,
    .release = block_example_release,
    .getgeo = block_example_getgeo,
};

static int __init block_example_init(void) {
    device_data = vmalloc(DEVICE_SECTORS * SECTOR_SIZE);
    if (!device_data) {
        return -ENOMEM;
    }

    major_num = register_blkdev(major_num, DEVICE_NAME);
    if (major_num <= 0) {
        vfree(device_data);
        return -EBUSY;
    }

    queue = blk_init_queue(block_example_request, NULL);
    if (!queue) {
        unregister_blkdev(major_num, DEVICE_NAME);
        vfree(device_data);
        return -ENOMEM;
    }

    gd = alloc_disk(1);
    if (!gd) {
        blk_cleanup_queue(queue);
        unregister_blkdev(major_num, DEVICE_NAME);
        vfree(device_data);
        return -ENOMEM;
    }

    gd->major = major_num;
    gd->first_minor = 0;
    gd->fops = &block_example_ops;
    gd->queue = queue;
    snprintf(gd->disk_name, 32, DEVICE_NAME);
    set_capacity(gd, DEVICE_SECTORS);
    add_disk(gd);

    printk(KERN_INFO "block_example: module loaded\n");
    return 0;
}

static void __exit block_example_exit(void) {
    del_gendisk(gd);
    put_disk(gd);
    blk_cleanup_queue(queue);
    unregister_blkdev(major_num, DEVICE_NAME);
    vfree(device_data);
    printk(KERN_INFO "block_example: module unloaded\n");
}

module_init(block_example_init);
module_exit(block_example_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("A simple block device");
MODULE_AUTHOR("Your Name");
编译和加载内核模块
  1. 创建Makefile

    创建一个Makefile用于编译内核模块:

    obj-m += block_device_example.o
    
    all:
    	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
    
    clean:
    	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
  2. 编译模块

    在终端中运行以下命令来编译模块:

    make
  3. 加载模块

    使用以下命令加载内核模块:

    sudo insmod block_device_example.ko
  4. 创建设备节点

    使用以下命令创建设备节点:

    sudo mknod /dev/block_example b [major_number] 0

    请将[major_number]替换为加载模块时输出的主设备号。

用户空间测试

您可以使用dd命令与块设备进行交互:

  1. 写入数据到设备

    echo "Hello, Block Device!" | sudo dd of=/dev/block_example bs=512 count=1
  2. 从设备读取数据

    sudo dd if=/dev/block_example bs=512 count=1

    您应该看到输出:

    Hello, Block Device!
卸载模块
  1. 卸载模块

    使用以下命令卸载内核模块:

    sudo rmmod block_device_example
  2. 删除设备节点

    使用以下命令删除设备节点:

    sudo rm /dev/block_example

说明

  • register_blkdev:注册一个块设备并获取一个主设备号。
  • blk_init_queue:初始化请求队列,用于处理I/O请求。
  • alloc_disk:分配并初始化一个gendisk结构,表示块设备。
  • blk_fetch_request:从请求队列中获取请求并处理。

内存映射(mmap)

   mmap系统调用允许应用程序将文件或设备映射到进程的地址空间,从而实现高效的数据传输。在Linux中,内存映射(mmap)可以用于在内核和用户空间之间共享内存。这种机制通常用于设备驱动程序,以便用户空间应用程序可以直接访问设备的内存区域。下面是一个简单的示例,演示如何在内核模块中实现内存映射,并在用户空间程序中访问该映射。

内核模块部分

我们将创建一个简单的字符设备驱动程序,允许用户空间程序通过mmap访问内核分配的内存。

内核模块代码

创建一个名为mmap_kernel_example.c的文件,并添加以下代码:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <linux/uaccess.h>

#define DEVICE_NAME "mmap_example"
#define CLASS_NAME "mmap_class"
#define MEM_SIZE 4096

static int majorNumber;
static struct class* mmapClass = NULL;
static struct device* mmapDevice = NULL;
static char *kernel_buffer;

static int mmap_open(struct inode *inodep, struct file *filep) {
    printk(KERN_INFO "mmap_example: Device opened\n");
    return 0;
}

static int mmap_release(struct inode *inodep, struct file *filep) {
    printk(KERN_INFO "mmap_example: Device closed\n");
    return 0;
}

static int mmap_mmap(struct file *filep, struct vm_area_struct *vma) {
    unsigned long pfn = virt_to_phys(kernel_buffer) >> PAGE_SHIFT;
    if (remap_pfn_range(vma, vma->vm_start, pfn, vma->vm_end - vma->vm_start, vma->vm_page_prot)) {
        return -EAGAIN;
    }
    return 0;
}

static struct file_operations fops = {
    .open = mmap_open,
    .release = mmap_release,
    .mmap = mmap_mmap,
};

static int __init mmap_example_init(void) {
    majorNumber = register_chrdev(0, DEVICE_NAME, &fops);
    if (majorNumber < 0) {
        printk(KERN_ALERT "mmap_example failed to register a major number\n");
        return majorNumber;
    }
    printk(KERN_INFO "mmap_example: registered correctly with major number %d\n", majorNumber);

    mmapClass = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(mmapClass)) {
        unregister_chrdev(majorNumber, DEVICE_NAME);
        printk(KERN_ALERT "Failed to register device class\n");
        return PTR_ERR(mmapClass);
    }
    printk(KERN_INFO "mmap_example: device class registered correctly\n");

    mmapDevice = device_create(mmapClass, NULL, MKDEV(majorNumber, 0), NULL, DEVICE_NAME);
    if (IS_ERR(mmapDevice)) {
        class_destroy(mmapClass);
        unregister_chrdev(majorNumber, DEVICE_NAME);
        printk(KERN_ALERT "Failed to create the device\n");
        return PTR_ERR(mmapDevice);
    }
    printk(KERN_INFO "mmap_example: device class created correctly\n");

    kernel_buffer = kmalloc(MEM_SIZE, GFP_KERNEL);
    if (!kernel_buffer) {
        device_destroy(mmapClass, MKDEV(majorNumber, 0));
        class_destroy(mmapClass);
        unregister_chrdev(majorNumber, DEVICE_NAME);
        printk(KERN_ALERT "Failed to allocate memory\n");
        return -ENOMEM;
    }
    strcpy(kernel_buffer, "Hello from kernel space!");

    return 0;
}

static void __exit mmap_example_exit(void) {
    kfree(kernel_buffer);
    device_destroy(mmapClass, MKDEV(majorNumber, 0));
    class_unregister(mmapClass);
    class_destroy(mmapClass);
    unregister_chrdev(majorNumber, DEVICE_NAME);
    printk(KERN_INFO "mmap_example: Goodbye from the LKM!\n");
}

module_init(mmap_example_init);
module_exit(mmap_example_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("A simple Linux char driver with mmap");
MODULE_AUTHOR("Your Name");

用户空间程序部分

创建一个名为mmap_user_example.c的文件,并添加以下代码:

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

#define DEVICE_FILE_NAME "/dev/mmap_example"
#define MEM_SIZE 4096

int main() {
    int fd;
    char *mapped_mem;

    fd = open(DEVICE_FILE_NAME, O_RDWR);
    if (fd < 0) {
        perror("Failed to open the device...");
        return errno;
    }

    mapped_mem = mmap(NULL, MEM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (mapped_mem == MAP_FAILED) {
        perror("mmap failed");
        close(fd);
        return errno;
    }

    printf("Data from kernel space: %s\n", mapped_mem);

    // Modify the data
    sprintf(mapped_mem, "Hello from user space!");

    // Clean up
    if (munmap(mapped_mem, MEM_SIZE) == -1) {
        perror("Error unmapping memory");
    }
    close(fd);

    return 0;
}
编译和运行
  1. 编译内核模块

    创建一个Makefile用于编译内核模块:

    obj-m += mmap_kernel_example.o
    
    all:
    	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
    
    clean:
    	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

    然后在终端中运行:

    make
  2. 编译用户空间程序

    使用以下命令编译用户空间程序:

    gcc -o mmap_user_example mmap_user_example.c
  3. 加载内核模块

    使用以下命令加载内核模块:

    sudo insmod mmap_kernel_example.ko
  4. 创建设备节点

    使用以下命令创建设备节点:

    sudo mknod /dev/mmap_example c [major_number] 0

    请将[major_number]替换为加载模块时输出的主设备号。

  5. 运行用户空间程序

    运行用户空间程序:

    ./mmap_user_example

    您应该看到输出:

    Data from kernel space: Hello from kernel space!
  6. 卸载模块

    使用以下命令卸载内核模块:

    sudo rmmod mmap_kernel_example
  7. 删除设备节点

    使用以下命令删除设备节点:

    sudo rm /dev/mmap_example

说明

  • kmalloc:在内核中分配内存。
  • virt_to_phys:将虚拟地址转换为物理地址。
  • remap_pfn_range:将内核内存映射到用户空间。
  • mmap:在用户空间程序中,将设备文件映射到进程的地址空间。

信号(Signals)

        信号是一种用于通知进程某个事件发生的机制,内核可以通过信号通知应用程序某些事件。在Linux中,信号是一种用于进程间通信的机制,可以用于通知进程发生了某种事件。内核模块可以通过信号与用户空间应用程序进行通信,通常用于通知应用程序某个事件的发生。

下面是一个简单的示例,演示如何在内核模块中使用信号与用户空间应用程序进行通信。

内核模块部分

我们将创建一个内核模块,它在某个条件下向用户空间进程发送信号。

内核模块代码

创建一个名为signal_kernel_example.c的文件,并添加以下代码:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/sched/signal.h>
#include <linux/pid.h>
#include <linux/kthread.h>
#include <linux/delay.h>

#define SIGNAL_NUM 44  // 自定义信号编号,通常在SIGRTMIN和SIGRTMAX之间

static struct task_struct *task;
static int pid = -1;
module_param(pid, int, 0644);

static int signal_thread(void *data) {
    struct pid *pid_struct;
    struct task_struct *task_struct;
    struct siginfo info;

    pid_struct = find_get_pid(pid);
    if (!pid_struct) {
        printk(KERN_INFO "signal_example: Invalid PID\n");
        return -1;
    }

    task_struct = pid_task(pid_struct, PIDTYPE_PID);
    if (!task_struct) {
        printk(KERN_INFO "signal_example: Could not find task\n");
        return -1;
    }

    memset(&info, 0, sizeof(struct siginfo));
    info.si_signo = SIGNAL_NUM;
    info.si_code = SI_QUEUE;
    info.si_int = 1234;  // 传递给用户空间的数据

    while (!kthread_should_stop()) {
        send_sig_info(SIGNAL_NUM, &info, task_struct);
        printk(KERN_INFO "signal_example: Signal sent to PID %d\n", pid);
        ssleep(5);  // 每5秒发送一次信号
    }

    return 0;
}

static int __init signal_example_init(void) {
    if (pid < 0) {
        printk(KERN_INFO "signal_example: Please provide a valid PID\n");
        return -1;
    }

    task = kthread_run(signal_thread, NULL, "signal_thread");
    if (IS_ERR(task)) {
        printk(KERN_INFO "signal_example: Failed to create kernel thread\n");
        return PTR_ERR(task);
    }

    printk(KERN_INFO "signal_example: Module loaded\n");
    return 0;
}

static void __exit signal_example_exit(void) {
    if (task) {
        kthread_stop(task);
    }
    printk(KERN_INFO "signal_example: Module unloaded\n");
}

module_init(signal_example_init);
module_exit(signal_example_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("A simple kernel module to send signals to user space");
MODULE_AUTHOR("Your Name");

用户空间程序部分

创建一个名为signal_user_example.c的文件,并添加以下代码:

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

void signal_handler(int signo, siginfo_t *info, void *extra) {
    printf("Received signal %d with value %d\n", signo, info->si_int);
}

int main() {
    struct sigaction action;
    action.sa_flags = SA_SIGINFO;
    action.sa_sigaction = signal_handler;

    if (sigaction(44, &action, NULL) == -1) {
        perror("sigaction");
        exit(EXIT_FAILURE);
    }

    printf("Waiting for signals...\n");
    while (1) {
        pause();  // 等待信号
    }

    return 0;
}
编译和运行
  1. 编译内核模块

    创建一个Makefile用于编译内核模块:

    obj-m += signal_kernel_example.o
    
    all:
    	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
    
    clean:
    	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

    然后在终端中运行:

    make
  2. 编译用户空间程序

    使用以下命令编译用户空间程序:

    gcc -o signal_user_example signal_user_example.c
  3. 运行用户空间程序

    在终端中运行用户空间程序:

    ./signal_user_example

    记下该程序的PID(可以通过echo $$获取)。

  4. 加载内核模块

    使用以下命令加载内核模块,并传递用户空间程序的PID:

    sudo insmod signal_kernel_example.ko pid=[user_program_pid]

    请将[user_program_pid]替换为用户空间程序的实际PID。

  5. 观察输出

    用户空间程序应该每隔5秒收到一个信号,并打印出信号信息。

  6. 卸载模块

    使用以下命令卸载内核模块:

    sudo rmmod signal_kernel_example

说明

  • send_sig_info:用于从内核向用户空间进程发送信号。
  • sigaction:在用户空间程序中设置信号处理程序。
  • kthread:内核线程用于定期发送信号

消息队列

     在Linux中,消息队列是一种进程间通信(IPC)机制,允许进程通过消息队列发送和接收消息。消息队列提供了一种在进程之间传递数据的方式,而不需要共享内存或使用信号。下面是一个简单的示例,演示如何在用户空间使用POSIX消息队列进行进程间通信。

用户空间程序示例

我们将创建两个程序:一个发送消息到消息队列,另一个从消息队列接收消息。

发送消息的程序

创建一个名为mq_sender.c的文件,并添加以下代码:

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

#define QUEUE_NAME  "/test_queue"
#define MAX_SIZE    1024
#define MSG_STOP    "exit"

int main() {
    mqd_t mq;
    char buffer[MAX_SIZE];

    // Open the message queue
    mq = mq_open(QUEUE_NAME, O_WRONLY | O_CREAT, 0644, NULL);
    if (mq == (mqd_t)-1) {
        perror("mq_open");
        exit(1);
    }

    printf("Enter messages to send to the queue (type 'exit' to stop):\n");

    while (1) {
        printf("> ");
        fgets(buffer, MAX_SIZE, stdin);

        // Remove newline character
        buffer[strcspn(buffer, "\n")] = '\0';

        // Send the message
        if (mq_send(mq, buffer, strlen(buffer) + 1, 0) == -1) {
            perror("mq_send");
            exit(1);
        }

        // Exit loop if the message is "exit"
        if (strncmp(buffer, MSG_STOP, strlen(MSG_STOP)) == 0) {
            break;
        }
    }

    // Close the message queue
    mq_close(mq);

    return 0;
}
接收消息的程序

创建一个名为mq_receiver.c的文件,并添加以下代码:

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

#define QUEUE_NAME  "/test_queue"
#define MAX_SIZE    1024
#define MSG_STOP    "exit"

int main() {
    mqd_t mq;
    char buffer[MAX_SIZE + 1];
    ssize_t bytes_read;

    // Open the message queue
    mq = mq_open(QUEUE_NAME, O_RDONLY);
    if (mq == (mqd_t)-1) {
        perror("mq_open");
        exit(1);
    }

    printf("Waiting for messages...\n");

    while (1) {
        // Receive the message
        bytes_read = mq_receive(mq, buffer, MAX_SIZE, NULL);
        if (bytes_read >= 0) {
            buffer[bytes_read] = '\0';
            printf("Received: %s\n", buffer);

            // Exit loop if the message is "exit"
            if (strncmp(buffer, MSG_STOP, strlen(MSG_STOP)) == 0) {
                break;
            }
        } else {
            perror("mq_receive");
            exit(1);
        }
    }

    // Close and unlink the message queue
    mq_close(mq);
    mq_unlink(QUEUE_NAME);

    return 0;
}
编译和运行
  1. 编译发送程序

    使用以下命令编译发送程序:

    gcc -o mq_sender mq_sender.c -lrt
  2. 编译接收程序

    使用以下命令编译接收程序:

    gcc -o mq_receiver mq_receiver.c -lrt
  3. 运行接收程序

    在一个终端中运行接收程序:

    ./mq_receiver
  4. 运行发送程序

    在另一个终端中运行发送程序:

    ./mq_sender

    输入消息并按回车发送。输入exit以停止发送程序。

  5. 观察输出

    接收程序将显示接收到的消息。

解释

  • mq_open:打开或创建一个消息队列。
  • mq_send:发送消息到消息队列。
  • mq_receive:从消息队列接收消息。
  • mq_close:关闭消息队列。
  • mq_unlink:删除消息队列。

       这个示例展示了如何使用POSIX消息队列在两个用户空间进程之间进行通信。消息队列提供了一种简单而有效的方式来在进程之间传递数据,适用于需要异步通信的场景。

         这些是传统的进程间通信(IPC)机制,允许内核和用户空间进程之间进行复杂的数据交换。在Linux中,内核与用户空间之间的通信通常不使用传统的用户空间消息队列(如POSIX消息队列),因为这些机制主要用于用户空间进程之间的通信。相反,内核与用户空间的通信通常通过其他机制实现,如Netlink套接字、ioctlprocfssysfsmmap等。然而,如果您希望在内核和用户空间之间实现类似消息队列的功能,可以使用Netlink套接字。这是一种专门设计用于内核和用户空间之间通信的机制,支持异步消息传递。

内核模块部分

创建一个名为netlink_kernel_example.c的文件,并添加以下代码:

#include <linux/module.h>
#include <linux/netlink.h>
#include <linux/skbuff.h>
#include <net/sock.h>

#define NETLINK_USER 31

struct sock *nl_sk = NULL;

static void nl_recv_msg(struct sk_buff *skb) {
    struct nlmsghdr *nlh;
    int pid;
    struct sk_buff *skb_out;
    int msg_size;
    char *msg = "Hello from kernel";
    int res;

    nlh = (struct nlmsghdr *)skb->data;
    printk(KERN_INFO "Netlink received msg payload: %s\n", (char *)nlmsg_data(nlh));
    pid = nlh->nlmsg_pid; /*pid of sending process */

    msg_size = strlen(msg);

    skb_out = nlmsg_new(msg_size, 0);
    if (!skb_out) {
        printk(KERN_ERR "Failed to allocate new skb\n");
        return;
    }

    nlh = nlmsg_put(skb_out, 0, 0, NLMSG_DONE, msg_size, 0);
    NETLINK_CB(skb_out).dst_group = 0; /* not in mcast group */
    strncpy(nlmsg_data(nlh), msg, msg_size);

    res = nlmsg_unicast(nl_sk, skb_out, pid);
    if (res < 0)
        printk(KERN_INFO "Error while sending back to user\n");
}

static int __init netlink_init(void) {
    struct netlink_kernel_cfg cfg = {
        .input = nl_recv_msg,
    };

    nl_sk = netlink_kernel_create(&init_net, NETLINK_USER, &cfg);
    if (!nl_sk) {
        printk(KERN_ALERT "Error creating socket.\n");
        return -10;
    }

    printk(KERN_INFO "Netlink example module inserted.\n");
    return 0;
}

static void __exit netlink_exit(void) {
    netlink_kernel_release(nl_sk);
    printk(KERN_INFO "Netlink example module removed.\n");
}

module_init(netlink_init);
module_exit(netlink_exit);

MODULE_LICENSE("GPL");

用户空间程序部分

创建一个名为netlink_user_example.c的文件,并添加以下代码:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <linux/netlink.h>

#define NETLINK_USER 31
#define MAX_PAYLOAD 1024 /* maximum payload size*/

int main() {
    struct sockaddr_nl src_addr, dest_addr;
    struct nlmsghdr *nlh = NULL;
    struct iovec iov;
    int sock_fd;
    struct msghdr msg;

    sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_USER);
    if (sock_fd < 0) {
        return -1;
    }

    memset(&src_addr, 0, sizeof(src_addr));
    src_addr.nl_family = AF_NETLINK;
    src_addr.nl_pid = getpid(); /* self pid */

    bind(sock_fd, (struct sockaddr *)&src_addr, sizeof(src_addr));

    memset(&dest_addr, 0, sizeof(dest_addr));
    dest_addr.nl_family = AF_NETLINK;
    dest_addr.nl_pid = 0; /* For Linux Kernel */
    dest_addr.nl_groups = 0; /* unicast */

    nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
    memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
    nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
    nlh->nlmsg_pid = getpid();
    nlh->nlmsg_flags = 0;

    strcpy(NLMSG_DATA(nlh), "Hello from user space");

    iov.iov_base = (void *)nlh;
    iov.iov_len = nlh->nlmsg_len;
    msg.msg_name = (void *)&dest_addr;
    msg.msg_namelen = sizeof(dest_addr);
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;

    sendmsg(sock_fd, &msg, 0);

    /* Read message from kernel */
    recvmsg(sock_fd, &msg, 0);
    printf("Received message payload: %s\n", (char *)NLMSG_DATA(nlh));

    close(sock_fd);
    return 0;
}
编译和运行
  1. 编译内核模块

    创建一个Makefile用于编译内核模块:

    obj-m += netlink_kernel_example.o
    
    all:
    	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
    
    clean:
    	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

    然后在终端中运行:

    make
  2. 编译用户空间程序

    使用以下命令编译用户空间程序:

    gcc -o netlink_user_example netlink_user_example.c
  3. 加载内核模块

    使用以下命令加载内核模块:

    sudo insmod netlink_kernel_example.ko
  4. 运行用户空间程序

    运行用户空间程序:

    ./netlink_user_example

    您应该看到输出:

    Received message payload: Hello from kernel
  5. 卸载模块

    使用以下命令卸载内核模块:

    sudo rmmod netlink_kernel_example

解释

  • Netlink Socket:在内核和用户空间之间创建通信通道。
  • nl_recv_msg:内核模块中的回调函数,用于处理来自用户空间的消息。
  • sendmsg/recvmsg:用户空间程序使用这些函数发送和接收Netlink消息。

这个示例展示了如何使用Netlink在内核和用户空间之间进行简单的消息传递。Netlink的强大之处在于它支持复杂的通信模式和大规模的数据传输,广泛用于网络配置和监控等场景。

信号量

      在Linux中,信号量通常用于进程间或线程间的同步,而不是直接用于内核和用户空间之间的通信。内核和用户空间之间的通信通常通过其他机制实现,如Netlink、ioctlprocfssysfs等。然而,如果您希望在内核和用户空间之间实现同步,可以使用信号量来控制对共享资源的访问。下面是一个示例,演示如何在内核模块中使用信号量来同步对共享资源的访问,同时通过procfs与用户空间进行简单的交互。

内核模块部分

我们将创建一个内核模块,使用信号量来保护对共享计数器的访问,并通过procfs与用户空间进行交互。

内核模块代码

创建一个名为sem_kernel_example.c的文件,并添加以下代码:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/proc_fs.h>
#include <linux/uaccess.h>
#include <linux/semaphore.h>

#define PROC_NAME "sem_example"
#define BUFFER_SIZE 128

static struct semaphore sem;
static int counter = 0;
static char proc_buffer[BUFFER_SIZE];

static ssize_t proc_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos) {
    int len = 0;

    if (*ppos > 0 || count < BUFFER_SIZE)
        return 0;

    down(&sem);  // Acquire the semaphore
    len += sprintf(proc_buffer, "Counter: %d\n", counter);
    up(&sem);    // Release the semaphore

    if (copy_to_user(ubuf, proc_buffer, len))
        return -EFAULT;

    *ppos = len;
    return len;
}

static ssize_t proc_write(struct file *file, const char __user *ubuf, size_t count, loff_t *ppos) {
    char buf[BUFFER_SIZE];

    if (count > BUFFER_SIZE)
        return -EFAULT;

    if (copy_from_user(buf, ubuf, count))
        return -EFAULT;

    buf[count] = '\0';

    down(&sem);  // Acquire the semaphore
    sscanf(buf, "%d", &counter);
    up(&sem);    // Release the semaphore

    return count;
}

static struct file_operations proc_ops = {
    .owner = THIS_MODULE,
    .read = proc_read,
    .write = proc_write,
};

static int __init sem_example_init(void) {
    sema_init(&sem, 1);  // Initialize the semaphore with a count of 1

    if (!proc_create(PROC_NAME, 0666, NULL, &proc_ops)) {
        printk(KERN_ERR "Failed to create /proc/%s\n", PROC_NAME);
        return -ENOMEM;
    }

    printk(KERN_INFO "/proc/%s created\n", PROC_NAME);
    return 0;
}

static void __exit sem_example_exit(void) {
    remove_proc_entry(PROC_NAME, NULL);
    printk(KERN_INFO "/proc/%s removed\n", PROC_NAME);
}

module_init(sem_example_init);
module_exit(sem_example_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Semaphore example with procfs");
MODULE_AUTHOR("Your Name");

用户空间程序部分

用户空间程序可以通过/proc/sem_example文件与内核模块进行交互。

读取和写入计数器

您可以使用以下命令与内核模块进行交互:

  1. 读取计数器值

    cat /proc/sem_example
  2. 写入计数器值

    echo 42 | sudo tee /proc/sem_example
编译和运行
  1. 编译内核模块

    创建一个Makefile用于编译内核模块:

    obj-m += sem_kernel_example.o
    
    all:
    	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
    
    clean:
    	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

    然后在终端中运行:

    make
  2. 加载内核模块

    使用以下命令加载内核模块:

    sudo insmod sem_kernel_example.ko
  3. 与模块交互

    使用上面的命令与模块交互。

  4. 卸载模块

    使用以下命令卸载内核模块:

    sudo rmmod sem_kernel_example

说明

  • sema_init:初始化信号量。
  • down:获取信号量,进入临界区。
  • up:释放信号量,退出临界区。
  • proc_create:创建一个/proc文件,用于与用户空间交互。

这个示例展示了如何在内核模块中使用信号量来同步对共享资源的访问,并通过procfs与用户空间进行简单的交互。信号量用于确保对共享计数器的访问是线程安全的。

信号量(Semaphore)进程间通信例子

       信号量(Semaphore)是一种用于进程间同步的机制,常用于控制对共享资源的访问。在Linux中,信号量可以用于用户空间进程之间的同步,也可以用于内核模块中实现同步机制。

下面是一个简单的用户空间信号量示例,演示如何使用POSIX信号量在两个进程之间进行同步。

用户空间信号量示例

我们将创建两个程序:一个程序将增加信号量,另一个程序将减少信号量。

初始化和增加信号量的程序

创建一个名为sem_post_example.c的文件,并添加以下代码:

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

#define SEM_NAME "/example_semaphore"

int main() {
    sem_t *sem;

    // Open or create a named semaphore
    sem = sem_open(SEM_NAME, O_CREAT, 0644, 0);
    if (sem == SEM_FAILED) {
        perror("sem_open");
        exit(EXIT_FAILURE);
    }

    printf("Press Enter to post (increase) the semaphore...\n");
    getchar();

    // Increase the semaphore
    if (sem_post(sem) < 0) {
        perror("sem_post");
        exit(EXIT_FAILURE);
    }

    printf("Semaphore posted.\n");

    // Close the semaphore
    sem_close(sem);

    return 0;
}
减少信号量的程序

创建一个名为sem_wait_example.c的文件,并添加以下代码:

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

#define SEM_NAME "/example_semaphore"

int main() {
    sem_t *sem;

    // Open the named semaphore
    sem = sem_open(SEM_NAME, 0);
    if (sem == SEM_FAILED) {
        perror("sem_open");
        exit(EXIT_FAILURE);
    }

    printf("Waiting for semaphore...\n");

    // Decrease the semaphore
    if (sem_wait(sem) < 0) {
        perror("sem_wait");
        exit(EXIT_FAILURE);
    }

    printf("Semaphore acquired.\n");

    // Close the semaphore
    sem_close(sem);

    return 0;
}
编译和运行
  1. 编译增加信号量的程序

    使用以下命令编译程序:

    gcc -o sem_post_example sem_post_example.c -pthread
  2. 编译减少信号量的程序

    使用以下命令编译程序:

    gcc -o sem_wait_example sem_wait_example.c -pthread
  3. 运行减少信号量的程序

    在一个终端中运行减少信号量的程序:

    ./sem_wait_example

    该程序将等待信号量。

  4. 运行增加信号量的程序

    在另一个终端中运行增加信号量的程序:

    ./sem_post_example

    按下回车键后,信号量将增加,等待的程序将继续执行。

  5. 清理信号量

    在完成测试后,可以使用以下命令删除命名信号量:

    sem_unlink("/example_semaphore")

说明

  • sem_open:打开或创建一个命名信号量。
  • sem_post:增加信号量的值,释放一个等待的进程。
  • sem_wait:减少信号量的值,如果信号量为0,则阻塞直到信号量大于0。
  • sem_close:关闭信号量。
  • sem_unlink:删除命名信号量。

这个示例展示了如何使用POSIX信号量在两个用户空间进程之间进行同步。信号量是一个强大的同步工具,适用于需要控制对共享资源访问的场景。

共享内存(System V IPC)

进程间通信例子

       共享内存是一种高效的进程间通信(IPC)机制,允许多个进程访问同一块内存区域。System V共享内存是Linux中实现共享内存的一种方式。下面是一个简单的示例,演示如何使用System V共享内存在两个用户空间进程之间进行通信。

使用System V共享内存

我们将创建两个程序:一个程序写入共享内存,另一个程序读取共享内存。

写入共享内存的程序

创建一个名为shm_writer.c的文件,并添加以下代码:

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

#define SHM_KEY 12345
#define SHM_SIZE 1024

int main() {
    int shmid;
    char *shmaddr;

    // 创建共享内存段
    shmid = shmget(SHM_KEY, SHM_SIZE, IPC_CREAT | 0666);
    if (shmid < 0) {
        perror("shmget");
        exit(EXIT_FAILURE);
    }

    // 将共享内存段附加到进程的地址空间
    shmaddr = shmat(shmid, NULL, 0);
    if (shmaddr == (char *)-1) {
        perror("shmat");
        exit(EXIT_FAILURE);
    }

    // 写入数据到共享内存
    snprintf(shmaddr, SHM_SIZE, "Hello from writer!");

    printf("Data written to shared memory: %s\n", shmaddr);

    // 分离共享内存段
    if (shmdt(shmaddr) == -1) {
        perror("shmdt");
        exit(EXIT_FAILURE);
    }

    return 0;
}
读取共享内存的程序

创建一个名为shm_reader.c的文件,并添加以下代码:

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

#define SHM_KEY 12345
#define SHM_SIZE 1024

int main() {
    int shmid;
    char *shmaddr;

    // 获取共享内存段
    shmid = shmget(SHM_KEY, SHM_SIZE, 0666);
    if (shmid < 0) {
        perror("shmget");
        exit(EXIT_FAILURE);
    }

    // 将共享内存段附加到进程的地址空间
    shmaddr = shmat(shmid, NULL, 0);
    if (shmaddr == (char *)-1) {
        perror("shmat");
        exit(EXIT_FAILURE);
    }

    // 读取数据从共享内存
    printf("Data read from shared memory: %s\n", shmaddr);

    // 分离共享内存段
    if (shmdt(shmaddr) == -1) {
        perror("shmdt");
        exit(EXIT_FAILURE);
    }

    // 删除共享内存段
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl");
        exit(EXIT_FAILURE);
    }

    return 0;
}
编译和运行
  1. 编译写入程序

    使用以下命令编译写入程序:

    gcc -o shm_writer shm_writer.c
  2. 编译读取程序

    使用以下命令编译读取程序:

    gcc -o shm_reader shm_reader.c
  3. 运行写入程序

    在一个终端中运行写入程序:

    ./shm_writer

    该程序将数据写入共享内存。

  4. 运行读取程序

    在另一个终端中运行读取程序:

    ./shm_reader

    该程序将从共享内存读取数据并显示。

说明

  • shmget:创建或获取一个共享内存段。
  • shmat:将共享内存段附加到进程的地址空间。
  • shmdt:分离共享内存段。
  • shmctl:控制共享内存段(在这里用于删除共享内存段)。

内核和应用层例子

在Linux中,实现内核和用户空间之间的共享内存通常不是通过传统的System V共享内存,而是通过mmap机制。这种方法允许用户空间进程直接访问内核分配的内存区域,从而实现高效的数据传输。

下面是一个示例,演示如何在内核模块中使用mmap来实现内核和用户空间之间的共享内存。

内核模块部分

我们将创建一个简单的字符设备驱动程序,允许用户空间程序通过mmap访问内核分配的内存。

内核模块代码

创建一个名为mmap_kernel_example.c的文件,并添加以下代码:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <linux/uaccess.h>

#define DEVICE_NAME "mmap_example"
#define CLASS_NAME "mmap_class"
#define MEM_SIZE 4096

static int majorNumber;
static struct class* mmapClass = NULL;
static struct device* mmapDevice = NULL;
static char *kernel_buffer;

static int mmap_open(struct inode *inodep, struct file *filep) {
    printk(KERN_INFO "mmap_example: Device opened\n");
    return 0;
}

static int mmap_release(struct inode *inodep, struct file *filep) {
    printk(KERN_INFO "mmap_example: Device closed\n");
    return 0;
}

static int mmap_mmap(struct file *filep, struct vm_area_struct *vma) {
    unsigned long pfn = virt_to_phys(kernel_buffer) >> PAGE_SHIFT;
    if (remap_pfn_range(vma, vma->vm_start, pfn, vma->vm_end - vma->vm_start, vma->vm_page_prot)) {
        return -EAGAIN;
    }
    return 0;
}

static struct file_operations fops = {
    .open = mmap_open,
    .release = mmap_release,
    .mmap = mmap_mmap,
};

static int __init mmap_example_init(void) {
    majorNumber = register_chrdev(0, DEVICE_NAME, &fops);
    if (majorNumber < 0) {
        printk(KERN_ALERT "mmap_example failed to register a major number\n");
        return majorNumber;
    }
    printk(KERN_INFO "mmap_example: registered correctly with major number %d\n", majorNumber);

    mmapClass = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(mmapClass)) {
        unregister_chrdev(majorNumber, DEVICE_NAME);
        printk(KERN_ALERT "Failed to register device class\n");
        return PTR_ERR(mmapClass);
    }
    printk(KERN_INFO "mmap_example: device class registered correctly\n");

    mmapDevice = device_create(mmapClass, NULL, MKDEV(majorNumber, 0), NULL, DEVICE_NAME);
    if (IS_ERR(mmapDevice)) {
        class_destroy(mmapClass);
        unregister_chrdev(majorNumber, DEVICE_NAME);
        printk(KERN_ALERT "Failed to create the device\n");
        return PTR_ERR(mmapDevice);
    }
    printk(KERN_INFO "mmap_example: device class created correctly\n");

    kernel_buffer = kmalloc(MEM_SIZE, GFP_KERNEL);
    if (!kernel_buffer) {
        device_destroy(mmapClass, MKDEV(majorNumber, 0));
        class_destroy(mmapClass);
        unregister_chrdev(majorNumber, DEVICE_NAME);
        printk(KERN_ALERT "Failed to allocate memory\n");
        return -ENOMEM;
    }
    strcpy(kernel_buffer, "Hello from kernel space!");

    return 0;
}

static void __exit mmap_example_exit(void) {
    kfree(kernel_buffer);
    device_destroy(mmapClass, MKDEV(majorNumber, 0));
    class_unregister(mmapClass);
    class_destroy(mmapClass);
    unregister_chrdev(majorNumber, DEVICE_NAME);
    printk(KERN_INFO "mmap_example: Goodbye from the LKM!\n");
}

module_init(mmap_example_init);
module_exit(mmap_example_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("A simple Linux char driver with mmap");
MODULE_AUTHOR("Your Name");

用户空间程序部分

创建一个名为mmap_user_example.c的文件,并添加以下代码:

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

#define DEVICE_FILE_NAME "/dev/mmap_example"
#define MEM_SIZE 4096

int main() {
    int fd;
    char *mapped_mem;

    fd = open(DEVICE_FILE_NAME, O_RDWR);
    if (fd < 0) {
        perror("Failed to open the device...");
        return errno;
    }

    mapped_mem = mmap(NULL, MEM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (mapped_mem == MAP_FAILED) {
        perror("mmap failed");
        close(fd);
        return errno;
    }

    printf("Data from kernel space: %s\n", mapped_mem);

    // Modify the data
    sprintf(mapped_mem, "Hello from user space!");

    // Clean up
    if (munmap(mapped_mem, MEM_SIZE) == -1) {
        perror("Error unmapping memory");
    }
    close(fd);

    return 0;
}
编译和运行
  1. 编译内核模块

    创建一个Makefile用于编译内核模块:

    obj-m += mmap_kernel_example.o
    
    all:
    	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
    
    clean:
    	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

    然后在终端中运行:

    make
  2. 编译用户空间程序

    使用以下命令编译用户空间程序:

    gcc -o mmap_user_example mmap_user_example.c
  3. 加载内核模块

    使用以下命令加载内核模块:

    sudo insmod mmap_kernel_example.ko
  4. 创建设备节点

    使用以下命令创建设备节点:

    sudo mknod /dev/mmap_example c [major_number] 0

    请将[major_number]替换为加载模块时输出的主设备号。

  5. 运行用户空间程序

    运行用户空间程序:

    ./mmap_user_example

    您应该看到输出:

    Data from kernel space: Hello from kernel space!
  6. 卸载模块

    使用以下命令卸载内核模块:

    sudo rmmod mmap_kernel_example
  7. 删除设备节点

    使用以下命令删除设备节点:

    sudo rm /dev/mmap_example

说明

  • kmalloc:在内核中分配内存。
  • virt_to_phys:将虚拟地址转换为物理地址。
  • remap_pfn_range:将内核内存映射到用户空间。
  • mmap:在用户空间程序中,将设备文件映射到进程的地址空间。

内核事件通知机制(如inotify、fanotify)

        这些机制允许应用程序监控文件系统事件,由内核通知应用程序。每种通信方式都有其适用的场景和优缺点,选择合适的机制取决于具体的应用需求和性能考虑。inotifyfanotify是Linux内核提供的两种文件系统事件通知机制,允许用户空间应用程序监控文件系统事件,如文件的创建、删除、修改等。

使用 inotify 监控文件系统事件

inotify 是一个强大的工具,用于监控文件系统中的事件。以下是一个使用 inotify 的用户空间程序示例。

示例:使用 inotify 监控文件

创建一个名为 inotify_example.c 的文件,并添加以下代码:

#include <stdio.h>
#include <stdlib.h>
#include <sys/inotify.h>
#include <unistd.h>
#include <limits.h>

#define EVENT_SIZE  (sizeof(struct inotify_event))
#define EVENT_BUF_LEN     (1024 * (EVENT_SIZE + 16))

int main() {
    int length, i = 0;
    int fd;
    int wd;
    char buffer[EVENT_BUF_LEN];

    // 创建 inotify 实例
    fd = inotify_init();
    if (fd < 0) {
        perror("inotify_init");
        exit(EXIT_FAILURE);
    }

    // 添加监控的目录或文件
    wd = inotify_add_watch(fd, "/tmp", IN_CREATE | IN_DELETE | IN_MODIFY);
    if (wd == -1) {
        perror("inotify_add_watch");
        exit(EXIT_FAILURE);
    }

    printf("Monitoring /tmp for changes...\n");

    // 读取事件
    while ((length = read(fd, buffer, EVENT_BUF_LEN)) > 0) {
        i = 0;
        while (i < length) {
            struct inotify_event *event = (struct inotify_event *)&buffer[i];
            if (event->len) {
                if (event->mask & IN_CREATE) {
                    printf("The file %s was created.\n", event->name);
                } else if (event->mask & IN_DELETE) {
                    printf("The file %s was deleted.\n", event->name);
                } else if (event->mask & IN_MODIFY) {
                    printf("The file %s was modified.\n", event->name);
                }
            }
            i += EVENT_SIZE + event->len;
        }
    }

    // 移除监控
    inotify_rm_watch(fd, wd);
    close(fd);

    return 0;
}
编译和运行
  1. 编译程序

    使用以下命令编译程序:

    gcc -o inotify_example inotify_example.c
  2. 运行程序

    运行程序:

    ./inotify_example

    该程序将监控 /tmp 目录中的文件创建、删除和修改事件。

使用 fanotify 监控文件系统事件

fanotify 是一种更高级的文件系统事件通知机制,支持更广泛的事件类型和更复杂的监控需求。

示例:使用 fanotify 监控文件

创建一个名为 fanotify_example.c 的文件,并添加以下代码:

#include <stdio.h>
#include <stdlib.h>
#include <sys/fanotify.h>
#include <unistd.h>
#include <limits.h>
#include <poll.h>

#define EVENT_SIZE  (sizeof(struct fanotify_event_metadata))
#define EVENT_BUF_LEN     (1024 * (EVENT_SIZE + 16))

int main() {
    int fanotify_fd;
    struct fanotify_event_metadata *metadata;
    char buffer[EVENT_BUF_LEN];
    struct pollfd fds[1];

    // 创建 fanotify 实例
    fanotify_fd = fanotify_init(FAN_CLOEXEC | FAN_CLASS_NOTIF, O_RDONLY);
    if (fanotify_fd < 0) {
        perror("fanotify_init");
        exit(EXIT_FAILURE);
    }

    // 添加监控的目录或文件
    if (fanotify_mark(fanotify_fd, FAN_MARK_ADD | FAN_MARK_MOUNT, FAN_OPEN | FAN_CLOSE, AT_FDCWD, "/tmp") == -1) {
        perror("fanotify_mark");
        exit(EXIT_FAILURE);
    }

    printf("Monitoring /tmp for open and close events...\n");

    fds[0].fd = fanotify_fd;
    fds[0].events = POLLIN;

    // 读取事件
    while (poll(fds, 1, -1) > 0) {
        int length = read(fanotify_fd, buffer, EVENT_BUF_LEN);
        if (length < 0) {
            perror("read");
            exit(EXIT_FAILURE);
        }

        metadata = (struct fanotify_event_metadata *)buffer;
        while (FAN_EVENT_OK(metadata, length)) {
            if (metadata->mask & FAN_OPEN) {
                printf("File opened: %d\n", metadata->fd);
            }
            if (metadata->mask & FAN_CLOSE) {
                printf("File closed: %d\n", metadata->fd);
            }
            close(metadata->fd);
            metadata = FAN_EVENT_NEXT(metadata, length);
        }
    }

    close(fanotify_fd);
    return 0;
}
编译和运行
  1. 编译程序

    使用以下命令编译程序:

    gcc -o fanotify_example fanotify_example.c
  2. 运行程序

    运行程序:

    sudo ./fanotify_example

    该程序将监控 /tmp 目录中的文件打开和关闭事件。

说明

  • inotify:适用于监控特定文件或目录的事件,使用简单,适合基本的文件系统事件监控。
  • fanotify:适用于更复杂的监控需求,可以监控整个文件系统的事件,支持更多的事件类型。

这两个示例展示了如何使用inotifyfanotify在用户空间程序中监控文件系统事件。选择哪种机制取决于您的具体需求和复杂性要求。