浅析Linux内核中的链表

2023-10-31

1.内核中的链表

linux内核链表与众不同,他不是把将数据结构塞入链表,而是将链表节点塞入数据,在2.1内核中引入了官方链表,从此内核中所有的链表使用都采用此链表,千万不要在重复造车轮子了!链表实现定义在<linux/list.h>,使用内核链表时,包含此文件。

1.1.传统的双向链表和内核中的双向链表的区别

  • 有个单独的头结点(head)
  • 每个节点(node)除了包含必要的数据之外,还有2个指针(pre,next)
  • pre指针指向前一个节点(node),next指针指向后一个节点(node)
  • 头结点(head)的pre指针指向链表的最后一个节点
  • 最后一个节点的next指针指向头结点(head)

传统的链表有个最大的缺点就是不好共通化,因为每个node中的data1,data2等等都是不确定的(无论是个数还是类型)。linux中的链表巧妙的解决了这个问题,linux的链表不是将用户数据保存在链表节点中,而是将链表节点保存在用户数据中.linux的链表节点只有2个指针(pre和next),这样的话,链表的节点将独立于用户数据之外,便于实现链表的共同操作。

1.2.链表基础数据结构

内核链表节点原型

/* linux/types.h */
struct list_head {
struct list_head *next, *prev;
};

gcc特有的语法支持,根据结构体成员和结构体,算出此成员所在结构体内的偏移量

#define list_entry(ptr, type, member) \
    container_of(ptr, type, member)

这个宏没什么特别的,主要是container_of这个宏

#define container_of(ptr, type, member) ({          \
    const typeof(((type *)0)->member)*__mptr = (ptr);    \
             (type *)((char *)__mptr - offsetof(type, member)); })

这里面的type一般是个结构体,也就是包含用户数据和链表节点的结构体。

ptr是指向type中链表节点的指针

member则是type中定义链表节点是用的名字

比如:

struct student
{
    int id;
    char* name;
    struct list_head list;
};
  • type是struct student
  • ptr是指向stuct list的指针,也就是指向member类型的指针
  • member就是 list
    ** 下面分析一下container_of宏: **
// 步骤1:将数字0强制转型为type*,然后取得其中的member元素
((type *)0)->member  // 相当于((struct student *)0)->list

// 步骤2:定义一个临时变量__mptr,并将其也指向ptr所指向的链表节点const typeof(((type *)0)->member)*__mptr = (ptr);

// 步骤3:计算member字段距离type中第一个字段的距离,也就是type地址和member地址之间的差
// offset(type, member)也是一个宏,定义如下:#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

// 步骤4:将__mptr的地址 - type地址和member地址之间的差
// 其实也就是获取type的地址

步骤1,2,4比较容易理解,下面的图以sturct student为例进行说明步骤3:

首先需要知道 ((TYPE *)0) 表示将地址0转换为 TYPE 类型的地址

由于TYPE的地址是0,所以((TYPE *)0)->MEMBER 也就是 MEMBER的地址和TYPE地址的差,如下图所示:

2.链表操作的主要函数

2.1.声明和初始化

实际上Linux只定义了链表节点,并没有专门定义链表头,那么一个链表结构是如何建立起来的呢?让我们来看看LIST_HEAD()这个宏

#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) struct list_head name = LIST_HEAD_INIT(name)

当我们用LIST_HEAD(nf_sockopts)声明一个名为nf_sockopts的链表头时,它的next、prev指针都初始化为指向自己,这样,我们就有了一个空链表,因为Linux用头指针的next是否指向自己来判断链表是否为空:

static inline int list_empty(const struct list_head *head)
{
return head->next == head;
}

除了用LIST_HEAD()宏在声明的时候初始化一个链表以外,Linux还提供了一个INIT_LIST_HEAD宏用于运行时初始化链表:

static inline void INIT_LIST_HEAD(struct list_head *list)
{

    list->next = list;
    list->prev = list;
}

2.2.插入/删除/合并

插入

对链表的插入操作有两种:在表头插入和在表尾插入。Linux为此提供了两个接口:

static inline void list_add(struct list_head *new, struct list_head *head);
static inline void list_add_tail(struct list_head *new, struct list_head *head);

因为Linux链表是循环表,且表头的next、prev分别指向链表中的第一个和最末一个节点,所以,list_add和list_add_tail的区别并不大,实际上,Linux分别用

__list_add(new, head, head->next); /*头插*/
__list_add(new, head->prev, head); /*尾插*/

来实现两个接口,可见,在表头插入是插入在head之后,而在表尾插入是插入在head->prev之后。

假设有一个新nf_sockopt_ops结构变量new_sockopt需要添加到nf_sockopts链表头,我们应当这样操作:

list_add(&new_sockopt.list, &nf_sockopts);

从这里我们看出,nf_sockopts链表中记录的并不是new_sockopt的地址,而是其中的list元素的地址。如何通过链表访问到new_sockopt呢?下面会有详细介绍。

删除

static inline void list_del(struct list_head *entry);

当我们需要删除nf_sockopts链表中添加的new_sockopt项时,我们这么操作:

list_del(&new_sockopt.list);

被剔除下来的new_sockopt.list,prev、next指针分别被设为LIST_POSITION2和LIST_POSITION1两个特殊值,这样设置是为了保证不在链表中的节点项不可访问–对LIST_POSITION1和LIST_POSITION2的访问都将引起页故障。与之相对应,list_del_init()函数将节点从链表

中解下来之后,调用LIST_INIT_HEAD()将节点置为空链状态。
搬移

Linux提供了将原本属于一个链表的节点移动到另一个链表的操作,并根据插入到新链表的位置分为两类:

tatic inline void list_move(struct list_head *list, struct list_head *head);
tatic inline void list_move_tail(struct list_head *list, struct list_head *head);

例如list_move(&new_sockopt.list,&nf_sockopts)会把new_sockopt从它所在的链表上删除,并将其再链入nf_sockopts的表头。

合并

除了针对节点的插入、删除操作,Linux链表还提供了整个链表的插入功能:

static inline void list_splice(struct list_head *list, struct list_head *head);

假设当前有两个链表,表头分别是list1和list2(都是struct list_head变量),当调用list_splice(&list1,&list2)时,只要list1非空,list1链表的内容将被挂接在list2链表上,位于list2和list2.next(原list2表的第一个节点)之间。新list2链表将以原list1表的第一个节点为首节点,而尾节点不变.

当list1被挂接到list2之后,作为原表头指针的list1的next、prev仍然指向原来的节点,为了避免引起混乱,Linux提供了一个list_splice_init()函数:

static inline void list_splice_init(struct list_head *list, struct list_head *head);

该函数在将list合并到head链表的基础上,调用INIT_LIST_HEAD(list)将list设置为空链。

遍历

我们知道,Linux链表中仅保存了数据项结构中list_head成员变量的地址,那么我们如何通过这个list_head成员访问到作为它的所有者的节点数据呢?Linux为此提供了一个list_entry(ptr,type,member)宏,其中ptr是指向该数据中list_head成员的指针,也就是
存储在链表中的地址值,type是数据项的类型,member则是数据项类型定义中list_head成员的变量名,例如,我们要访问nf_sockopts链表中首个nf_sockopt_ops变量,则如此调用:

list_entry(nf_sockopts->next, struct nf_sockopt_ops, list);

这里”list”正是nf_sockopt_ops结构中定义的用于链表操作的节点成员变量名。list_entry的使用相当简单,相比之下,它的实现则有一些难懂:

#define list_entry(ptr, type, member) container_of(ptr, type, member)
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})

在的nf_register_sockopt()函数中有这么一段话:

struct list_head *i;

list_for_each(i, &nf_sockopts) {
struct nf_sockopt_ops *ops = (struct nf_sockopt_ops *)i;

}

函数首先定义一个(struct list_head *)指针变量i,然后调用list_for_each(i,&nf_sockopts)进行遍历。在<include/linux/list.h>中,list_for_each()宏是这么定义的:

#define list_for_each(pos, head) \
for (pos = (head)->next, prefetch(pos->next); pos != (head); \
pos = pos->next, prefetch(pos->next))

它实际上是一个for循环,利用传入的pos作为循环变量,从表头head开始,逐项向后(next方向)移动pos,直至又回到head(prefetch()可以不考虑,用于预取以提高遍历速度)。

大多数情况下,遍历链表的时候都需要获得链表节点数据项,也就是说list_for_each()和list_entry()总是同时使用。对此Linux给出了一个list_for_each_entry()宏:

#define list_for_each_entry(pos, head, member)

某些应用需要反向遍历链表,Linux提供了list_for_each_prev()和list_for_each_entry_reverse()来完成这一操作,使用方法和上面介绍的list_for_each()、list_for_each_entry()完全相同。

安全性的考虑

在并发执行的环境下,链表操作通常都应该考虑同步安全性问题,为了方便,Linux将这一操作留给应用自己处理。Linux链表自己考虑的安全性主要有两个方面:

a list_empty()判断

基本的list_empty()仅以头指针的next是否指向自己来判断链表是否为空,Linux链表另行提了一个list_empty_careful()宏,它同时判断头指针的next和prev,仅当两者都指向自己时才返回真。这主要是为了应付另一个cpu正在处理同一个链表而造成next、prev不一致的情况。但代码注释也承认,这一安全保障能力有限:除非其他cpu的链表操作只有list_del_init(),否则仍然不能保证安全,也就是说,还是需要加锁保护。
b 遍历时节点删除
前面介绍了用于链表遍历的几个宏,它们都是通过移动pos指针来达到遍历的目的。但如果遍历的操作中包含删除pos指针所指向的节点,pos指针的移动就会被中断,因为list_del(pos)将把pos的next、prev置成LIST_POSITION2和LIST_POSITION1的特殊值。当然,调用者完全可以自己缓存next指针使遍历操作能够连贯起来,但为了编程的一致性,Linux链表仍然提供了两个对应于基本遍历操作的“_safe”接口:list_for_each_safe(pos,n, head)、list_for_each_entry_safe(pos, n, head, member),它们要求调用者另外提供一个与pos同类型的指针n,在for循环中暂存pos下一个节点的地址,避免因pos节点被释放而造成的断链。

3.例子

#include<linux/init.h>
#include<linux/slab.h>
#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/list.h>
MODULE_LICENSE("GPL");
struct student
{
	int id;
	char *name;
	struct list_head list;
};
void print_student(struct student *);
static int testlist_init(void)
{
	struct student *stu1, *stu2, *stu3, *stu4;
    struct student *stu;
	// init a list head
    LIST_HEAD(stu_head);
	// init four list nodes
    stu1 = kmalloc(sizeof(*stu1), GFP_KERNEL);
    stu1->id = 1;
    stu1->name = "wyb";
    INIT_LIST_HEAD(&stu1->list);
	stu2 = kmalloc(sizeof(*stu2), GFP_KERNEL);
    stu2->id = 2;
    stu2->name = "wyb2";
    INIT_LIST_HEAD(&stu2->list);
    stu3 = kmalloc(sizeof(*stu3), GFP_KERNEL);
    stu3->id = 3;
    stu3->name = "wyb3";
    INIT_LIST_HEAD(&stu3->list);
    stu4 = kmalloc(sizeof(*stu4), GFP_KERNEL);
    stu4->id = 4;
    stu4->name = "wyb4";
    INIT_LIST_HEAD(&stu4->list);
list_add (&stu1->list, &stu_head);
    list_add (&stu2->list, &stu_head);
    list_add (&stu3->list, &stu_head);
    list_add (&stu4->list, &stu_head);
	list_for_each_entry(stu, &stu_head, list)
    {
        print_student(stu);
    }
	// print each student from 1 to 4
    list_for_each_entry_reverse(stu, &stu_head, list)
    {
        print_student(stu);
    }
	 // delete a entry stu2
    list_del(&stu2->list);
    list_for_each_entry(stu, &stu_head, list)
    {
        print_student(stu);
    }
	// replace stu3 with stu2
    list_replace(&stu3->list, &stu2->list);
    list_for_each_entry(stu, &stu_head, list)
    {
        print_student(stu);
    }
	return 0;
}
static void testlist_exit(void)
{
    printk(KERN_ALERT "*************************\n");
    printk(KERN_ALERT "testlist is exited!\n");
    printk(KERN_ALERT "*************************\n");
}
void print_student(struct student *stu)
{
    printk (KERN_ALERT "======================\n");
    printk (KERN_ALERT "id  =%d\n", stu->id);
    printk (KERN_ALERT "name=%s\n", stu->name);
    printk (KERN_ALERT "======================\n");
}
module_init(testlist_init);
module_exit(testlist_exit);

【推荐阅读】 

了解ixgbe网卡驱动— 驱动注册(纯代码分享)

浅析linux 内核 高精度定时器(hrtimer)实现机制(一)

手把手教你如何编写一个Makefile文件

一文看懂页面置换算法

需要多久才能看完linux内核源码?

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

浅析Linux内核中的链表 的相关文章

  • 使用 inotify 的正确方法是什么?

    我想使用inotifyLinux 上的机制 我希望我的应用程序知道文件何时aaa被改变了 您能给我提供一个如何做到这一点的示例吗 文档 来自监视文件系统活动 inotify https developer ibm com tutorials
  • 如何将后台作业的输出分配给 bash 变量?

    我想在 bash 中运行后台作业并将其结果分配给一个变量 我不喜欢使用临时文件 并且希望同时运行多个类似的后台任务 root root var echo hello world root root echo var hello world
  • 码头无故停止

    我需要经验丰富的码头用户的建议 我在负载均衡器 亚马逊云 后面维护着 2 台 Linux 机器 使用 Jetty 9 0 3 有时我的 Jetty 容器会被 Thread 2 无故关闭 同时地 显示以下日志并且容器无故停止 没有错误 没有例
  • 从 php/linux 获取 pdf 的布局模式(横向或纵向)

    给定一个 PDF 如何使用 PHP lib 或 Linux 命令行工具获取 PDF 的布局模式 或相对宽度 高度 Using http www tecnick com public code cp dpage php aiocp dp tc
  • 未找到 Gem 命令

    我已经在 Ubuntu 10 10 32 位上安装了 gem apt get install gem y 但当我尝试跑步时 gem install something gem 我收到未找到命令的错误 bash gem command not
  • 如何设置Java线程的CPU核心亲和力?

    我搜索了以前关于类似主题的帖子 但找不到合适的答案 因此提出这个问题 非常感谢您帮助回答 我知道在 Linux 中通过任务集命令设置进程与特定 CPU 核心的关联性 但我想设置 Java 线程与特定 cpu 核心的亲和力 以便属于同一进程的
  • C++:Linux平台上的线程同步场景

    我正在为 Linux 平台实现多线程 C 程序 其中我需要类似于 WaitForMultipleObjects 的功能 在搜索解决方案时 我发现有一些文章描述了如何在 Linux 中实现 WaitForMultipleObjects 功能
  • 运行此处编译的 C 程序会导致在另一台服务器上找不到 GLIBC 库错误 - 是我的错还是他们的错?

    此处编译的 C 程序在我们的 Ubuntu 服务器上运行良好 但是当其他人尝试在他们的特定 Linux 服务器上运行它时 他们会收到以下错误 myprog install lib tls libc so 6 version GLIBC 2
  • 将node.js +expressjs应用程序的NODE_ENV设置为ubuntu下的守护进程

    我按照这些说明让守护进程正常工作 http kevin vanzonneveld net techblog article run nodejs as a service on ubuntu karmic http kevin vanzon
  • 在 debian wheezy amd64 上安装 ia32-libs

    我正在使用 Debian 7 喘息 amd64 uname a Linux tzwm debian 3 2 0 4 amd64 1 SMP Debian 3 2 51 1 x86 64 GNU Linux 我想安装ia32 libs在我的
  • 用于读取文件的 Bash 脚本

    不知道为什么最后一行没有从脚本中删除 bin bash FILENAME 1 while read line do cut d f2 echo line done lt FILENAME cat file 1 test 2 test 3 t
  • 使用 ProcessBuilder 运行 shell 脚本

    我正在尝试使用 Java 和 ProcessBuilder 运行脚本 当我尝试运行时 我收到以下消息 error 2 没有这样的文件或目录 我不知道我做错了什么 但这是我的代码 ps 我尝试只执行不带参数的脚本 错误是相同的 String
  • 如何在 Linux/OS X 上温和地终止 Firefox 进程

    我正在使用 Firefox 进行一些自动化操作 尽管我可以从 shell 打开 Firefox 窗口 但我无法正确终止它 如果我kill火狐进程与kill 3 or kill 2当我下次打开新的 Firefox 窗口时 命令会询问我是否要在
  • 套接字发送调用被阻塞很长时间

    我每 10 秒在套接字上发送 2 个字节的应用程序数据 阻塞 但发送调用在下面的最后一个实例中被阻塞超过 40 秒 2012 06 13 12 02 46 653417 信息 发送前 2012 06 13 12 02 46 653457 信
  • php_network_getaddresses: getaddrinfo 失败: 名称或服务未知 (0) 连接失败..!

    我正在使用 php 邮件程序功能 但出现以下错误 如何修复它 2016 01 22 06 15 48 SMTP 错误 无法连接到服务器 php network getaddresses getaddrinfo失败 名称或服务未知 0 连接失
  • 在用户程序中使用 或在驱动程序模块代码中使用 ...这有关系吗?

    我正在开发一个设备驱动程序模块和关联的用户库来处理ioctl 来电 该库获取相关信息并将其放入一个结构中 该结构被传递到驱动程序模块中并在那里解压 然后进行处理 我省略了很多步骤 但这就是总体思路 一些数据通过结构体传递ioctl is u
  • 由于 abi::cxx11 符号导致的链接问题?

    我们最近收到一份报告 因为GCC 5 1 libstdc 和双 ABI http gcc gnu org onlinedocs libstdc manual using dual abi html 它似乎Clang 不知道 GCC 内联名称
  • 如何使用 VSCode 调试 Linux 核心转储?

    我故意从我使用 VSCode 编写的 C 应用程序生成核心转储 我不知道如何调试核心转储 有没有人愿意分享这方面的经验 更新 我相信我现在已经可以使用了 我为核心文件创建了第二个调试配置 我需要添加指向生成的转储文件的 coreDumpPa
  • Unix 中的访问时间是多少

    我想知道访问时间是多少 我在网上搜索但得到了相同的定义 读 被改变 我知道与touch我们可以改变它 谁能用一个例子来解释一下它是如何改变的 有没有办法在unix中获取创建日期 时间 stat结构 The stat 2 结构跟踪所有文件日期
  • 如何在 bash_profile 文件中添加导出语句?

    我正在尝试了解是否必须添加导出语句来在 bash profile 文件中设置变量 我该怎么做呢 例如 如果我必须添加 export AX name 那么我应该将其简单地写在文件末尾还是我还需要编写其他内容 简单写一下export AS na

随机推荐

  • 记录--用js如何实现将手机号中间的几位数字变成****

    这里给大家分享我在网上总结出来的一些知识 希望对大家有所帮助 今天 我们要实现一个很常见并且简单的功能 将手机号中间的几位数变成 这个功能其实很常见 比如我们微信的账号安全里面显示的手机号 掘金的账号设置里面显示的手机号 支付宝里面的证件号
  • 安装 Protocol Buffer 2.5

    参考地址 https github com protocolbuffers protobuf releases tag v2 5 0 因 编译hadoop 源码 需要protocol 2 5 版本的环境 安装环境 windows10 1 下
  • 3DCAT携手华为,打造XR虚拟仿真实训实时云渲染解决方案

    2023年5月8日 9日 以 因聚而生 众志有为 为主题的 华为中国合作伙伴大会2023 在深圳国际会展中心隆重举行 本次大会汇聚了ICT产业界的广大新老伙伴朋友 共同探讨数字化转型的新机遇 共享数字化未来的新成果 华为中国合作伙伴大会20
  • 黑马Redis学习——实战篇(1)

    目录 1 短信登录 1 1 导入黑马点评项目 1 1 1 导入SQL 1 1 2 有关当前模型 1 1 3 导入后端项目 1 1 4 导入打开前端工程 1 2 基于Session实现登录流程 1 3 实现发送短信验证码功能 1 5 隐藏用户
  • 高中教学分析系统数据可视化探索【可视化实战案例】

    目录 前言 导入库 前言 教育行业中大数据分析的主要目的包括改善学生成绩 服务教务设计 优化学生服务等 而学生成绩中有一系列重要的信息往往被我们常规研究所忽视 通过大数据分析和可视化展示 挖掘重要信息 改善 学生服务 对于教学改进意义重大
  • Redis基本概念及配置(事务、持久化、主从复制、哨兵模式)

    Redis事务 Multi Exec discard 从输入Mulit命令开始 输入的命令都会进入命令队列中 但不会执行 直到输入Exec后 Redis将之前的队列中的命令依次执行 在命令组队过程中 可以使用discard放弃组队 如果某个
  • 无法找到元素 'aop:aspectj-autoproxy' 的声明

    通配符的匹配很全面 但无法找到元素 aop aspectj autoproxy 的声明 已解决 今天博主我在测试Spring Aop时遇到了一个在网上都很少见到的问题 是这样子的 当我执行Spring Aop测试代码时 它抛出了以下异常 o
  • 前端学习 C 语言 —— GDB调试器

    GDB调试器 我们在讲指针时用 GDB 调试段错误 本篇将详细介绍 gdb 的最常用命令 日志记录 检测点 最后介绍如何用 gdb 调试进程以及用gdb 调试一个开源项目的调试版本 glmark2 gdb介绍 GDB the GNU Pro
  • Android开发之RxJava使用

    RxJava是响应式编程 也可以理解为流式编程 核心是观察者模式 Rx是微软 Net的一个响应式扩展 Rx借助可观测的序列提供一种简单的方式来创建异步的 基于事件驱动的程序 2012年Netflix为了应对不断增长的业务需求开始将 NET
  • 华为OD机试 - 数字反转打印(Java)

    题目描述 小华是个对数字很敏感的小朋友 他觉得数字的不同排列方式有特殊美感 某天 小华突发奇想 如果数字多行排列 第一行1个数 第二行2个 第三行3个 即第n行有n个数字 并且奇数行正序排列 偶数行逆序排列 数字依次累加 这样排列的数字一定
  • AD9361配置采用纯PL方式,QT编写的小软件可以快速实现

    采用ADI官方的API函数 虽然能够快速的实现AD9361配置 让我们不必关注9361的内部寄存器的配置过程 但是在实际的项目开发过程中 也在一定程度上限制了AD9361与PL之间数据交互的灵活性 今天给大家推荐采用AD9361官方提供的配
  • Android开发之逐帧动画优化

    Android上如果使用逐帧动画的话 可以很方便地使用AnimationDrawable 无论是先声明xml还是直接代码里设置 都是几分钟的事 但使用AnimationDrawable有一个致命的弱点 那就是需要一次性加载所有图片到内存 万
  • go语言基础-----17-----channel创建、读写、安全关闭、多路复用select

    1 通道channel介绍 1 channel 可译为通道 是go语言协程goroutine之间的通信方式 2 channel通信可以想象成从管道的一头塞进数据 从另一头读取数据 通道作为容器是有限定大小的 满了就写不进去 空了就读不出来
  • 高防CDN对于网站、平台有着至关重要作用?

    1 减轻服务器的占用率和网站服务器的带宽资源 通过使用CDN服务 用户可以在CDN节点上分发对主要频道 包括页面和图片 的访问 这样可以减少源设备上的负载压力和带宽资源 并将资源保存到相同的带宽消耗服务中 如邮件 论坛和服务器资源 以保证网
  • 热区的使用方法

    1 如图所示 热区的位置是在元件库中 这样的一个标识 2 热区的使用经常会搭配一些比较小的文字或者图片等区域 只要是在热区中 随便点击哪一个地方都是属于这个区域 3 我们做了几个页面 样式不同 4 如图 我们创建一个热区在02的选区中 5
  • python + selenium实现巨潮资讯网指定范围年报下载

    大家好 第一次写文章 紧张滴捏 这段时间在做课设 课设里需要下载沪市600000到601000号的年报原文做数字化关键词的词频分析 想着用程序帮我批量下载一下 但是找了一下貌似没有类似的代码 就写了一个应用selenium库来做模拟下载的p
  • 各类打印机驱动官网下载安装

    前言概述 找驱动很简单 但是网上有时候找起来有点费劲呢 不安全 目前市面上打印机驱动搜索软件好用的基本都要付费 或者不全 比如下图这个就是付费的 常用的打印机品牌 惠普 HP 佳能 Canon 爱普生 Epson 京瓷 Kyocera 三星
  • 二流计算机学校,学校可以是二流的,但你不是

    我每天都会看大家在微博里给我的留言 时常看到深夜 私信的 每一条都看 问的最多的一种问题 是这么开头的 我的学校不好 或者 我是一个来自二 三 本学校的学生 我该怎么办 我不知道怎么回答 因为我不觉得来自一个二流的学校就应该过着二流的生活
  • C语言实现队列(链表实现)

    队列 Queue 也是运算受限的线性表 是一种先进先出 First In First Out 简称 FIFO 的线性表 只允许在表的一端进行插入 而在另一端进行删除 队首 front 允许进行删除的一端称为队首 队尾 rear 允许进行插入
  • 浅析Linux内核中的链表

    1 内核中的链表 linux内核链表与众不同 他不是把将数据结构塞入链表 而是将链表节点塞入数据 在2 1内核中引入了官方链表 从此内核中所有的链表使用都采用此链表 千万不要在重复造车轮子了 链表实现定义在