内核模块中字符驱动代码写的相当精简,提供了字符驱动的各种管理功能主要代码位于fs\char_dev.c文件中,分析该模块代码可以主要从三个方面入手:

  • 字符设备号管理:主要提供设备号注册申请等功能
  • struct cdev结构初始化等相关接口
  • 基于cdev管理:主要通过kobj_map结构将cdev结构 、open等操作与fops操作相关联以及设备添加

其中从管理数据结构出发主要为 char_device_struct、cdev、kobj_map

char_device_struct:主要是为了管理设备号分配使用情况,即管理dev_t

cdev: cdev结构主要是对外提供的一个数据结构

kobj_map:主要是字符设备管理中内部的一个数据结构,主要是用于管理cdev.

上述代码主要分为两个部分,笔者估计是和历史遗留有关系,因为dev的驱动模型早于sysfs设备管理模型,后来引入sysfs模型管理之后将fops以及设备管理等功能基于kobj管理进行了重构,个人认为可以将字符设备管理与kob进行一个统一,可能由于需要兼容原因一直没有人进行优化,且至于为什么这么设计笔者在内核邮件中暂时还没有找到相关说明。

设备号管理

设备号管理部分主要是基于struct char_device_struct结构进行管理,主要结构成员如下:

static struct char_device_struct {struct char_device_struct *next;unsigned int major;unsigned int baseminor;int minorct;char name[64];struct cdev *cdev;     /* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];
  • struct char_device_struct *next:该结构为一个单向链表,当在chrdevs经过hash计算之后在同一个数组内,则会将新的节点插入到char_device_struct中。
  • unsigned int major:该节点所属主字符设备号号
  • unsigned int baseminor:该节点次设备号开始位置。
  • int minorct:次设备号数量
  • char name:设备号名称
  • struct cdev *cdev:丢弃不用

整个结构归属与chrdevs变量中,该chrdevs为一个CHRDEV_MAJOR_HASH_SIZE小数组指针,CHRDEV_MAJOR_HASH_SIZE大小为255:

#define CHRDEV_MAJOR_HASH_SIZE 255

该结构是基于主设备号进行管理,每个结构内都申请由一个或者多个该主设备号内的次设备。整个chrdev数组大小为255,那么如何支持0~511范围的主设备号管理与分配?

其实内核在整个实现过程中由于历史原因需要保持对之前旧的255范围的兼容,有要将范围扩展到511,中间过程中使用了一个小技巧。

整个chrdev按照其范围从0~254依次排开:

  • 当主设备号为0~254范围时,将根据索引之间访问相应的chrdev数组指针,如果chrdev[i]其为空则说明没有分配,则申请char_device_struct结构大小内存将其地址赋值为chrdev[i]范围
  • 当主设备号范围大于255时,则需要首先求hsa值,计算方法为(index% CHRDEV_MAJOR_HASH_SIZE)求其余数,这样当index为255时,其访问数组为chrdev[255% CHRDEV_MAJOR_HASH_SIZE]=chrdev[0],和index 0指向同样的节点,此时chrdev[0]保存的时0 设备号主节点,需要将index为255新节点插入到该节点中,需要将其next指向0设备号节点,并将其chrdev[0],数组中,chrdev[0]永远指向其最近新申请的主设备号,如下所图

同样如此,256和1 保持在一个节点上,257和2保持在一个节点上,如此以来,如下所图:

即上图所示:0、255、510、765、...在节点0上,同样如此,1、256、511、...在节点1上,如此继续下去... ...,每个数组节点都是先按照主设备号由小到大进行排序。

如果其主设备号能够无限扩充,则会形成如上图所示的一个分布图,既能兼容之前旧的255范围,又能无限进行扩充。同时在无限扩充的基础上保持了该chrdev数组的规模以及没有增加其算法复杂度。

静态分配register_chrdev_region

register_chrdev_region()函数主要按照指定的设备号进行注册功能,函数代码如下:

int register_chrdev_region(dev_t from, unsigned count, const char *name)
{struct char_device_struct *cd;dev_t to = from + count;dev_t n, next;for (n = from; n < to; n = next) {next = MKDEV(MAJOR(n)+1, 0);if (next > to)next = to;cd = __register_chrdev_region(MAJOR(n), MINOR(n),next - n, name);if (IS_ERR(cd))goto fail;}return 0;
fail:to = n;for (n = from; n < to; n = next) {next = MKDEV(MAJOR(n)+1, 0);kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));}return PTR_ERR(cd);
}

函数入参 from为申请的起始设备号,count为申请的设备号数量,name为申请的设备名称

  • 该函数首先根据起始设备号和申请的设备号数量计算申请的末尾设备号: to = from + count
  • 进入循环依次将需要的设备号进行申请注册,由于主设备号占高12位,次设备号占低12位,首先需要获取到起始设备号的主设备号MAJOR(n),以及下一个主设备号next = MKDEV(MAJOR(n)+1, 0);
  • 将下一个主设备号与to做比较,申请的设备号数量是否都在同一个主设备号内,如果是同一个主设备号,则next>to,否则则表示位设备的设备并不属于同一个设备号
  • 调用__register_chrdev_region()函数 申请注册都在同一个主设备号内的设备。
  • 如果申请的设备数量不在同一个主设备号内,则继续循环,n=next,将申请注册下一个主设备号内的设备。

动态申请设备号alloc_chrdev_region

动态申请设备号为驱动开发人员不知道设备号整体使用情况,有char_dev模块动态申请所需要的设备号,代码如下:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name)
{struct char_device_struct *cd;cd = __register_chrdev_region(0, baseminor, count, name);if (IS_ERR(cd))return PTR_ERR(cd);*dev = MKDEV(cd->major, cd->baseminor);return 0;
}

dev为获取到的设备号,baseminor为次设备号起始位置,count为申请的设备号数量,name申请的设备号名称。

  • 该函数比较简单最终调用的是__register_chrdev_region函数,由于设备号提前不知道故传入的设备号参数为0

__register_chrdev_region()

无论是静态申请设备号还是动态申请设备号最终都是调用的__register_chrdev_region函数,该函数主要是基于同一个主设备号进行申请管理即在同一个主设备号上申请多个次设备功能。

static struct char_device_struct *
__register_chrdev_region(unsigned int major, unsigned int baseminor,int minorct, const char *name)
{struct char_device_struct *cd, *curr, *prev = NULL;int ret;int i;if (major >= CHRDEV_MAJOR_MAX) {pr_err("CHRDEV \"%s\" major requested (%u) is greater than the maximum (%u)\n",name, major, CHRDEV_MAJOR_MAX-1);return ERR_PTR(-EINVAL);}if (minorct > MINORMASK + 1 - baseminor) {pr_err("CHRDEV \"%s\" minor range requested (%u-%u) is out of range of maximum range (%u-%u) for a single major\n",name, baseminor, baseminor + minorct - 1, 0, MINORMASK);return ERR_PTR(-EINVAL);}cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);if (cd == NULL)return ERR_PTR(-ENOMEM);mutex_lock(&chrdevs_lock);if (major == 0) {ret = find_dynamic_major();if (ret < 0) {pr_err("CHRDEV \"%s\" dynamic allocation region is full\n",name);goto out;}major = ret;}ret = -EBUSY;i = major_to_index(major);for (curr = chrdevs[i]; curr; prev = curr, curr = curr->next) {if (curr->major < major)continue;if (curr->major > major)break;if (curr->baseminor + curr->minorct <= baseminor)continue;if (curr->baseminor >= baseminor + minorct)break;goto out;}cd->major = major;cd->baseminor = baseminor;cd->minorct = minorct;strlcpy(cd->name, name, sizeof(cd->name));if (!prev) {cd->next = curr;chrdevs[i] = cd;} else {cd->next = prev->next;prev->next = cd;}mutex_unlock(&chrdevs_lock);return cd;
out:mutex_unlock(&chrdevs_lock);kfree(cd);return ERR_PTR(ret);
}

__register_chrdev_region函数代码是设备号管理精髓,百行左右代码但是完成了很多功能:

  • 首先对参数入参主设备号和次设备号进行检查,当前linux 5.4.56内核版本中主设备号不能超过CHRDEV_MAJOR_MAX(CHRDEV_MAJOR_MAX大小为512定义在include\linux\fs.h文件中),次设备号不能超过MINORMASK(MINORMASK宏定义在include\linux\kdev_t.h文件中,大小为((1U << 20) - 1))。
  • 调用kzalloc结构,申请char_device_struct大小内存,地址复制为cd
  • mutex_lock(&chrdevs_lock)申请加锁,因为后续主要是对chrdevs全局静态数组操作。
  • 传入的major主设备号是否为0,如果为0 则表明为动态申请内存,则调用find_dynamic_majo()申请一个没有使用的主设备号。
  • 接下来是该该算法比较关键的地方,按照申请的major主设备号获取到所对应节点中的合适的prev,以及curr节点,以便为后面cd插入到合适的位置,整个cdrdev数组每个节点内的char_device_struct首先按照主设备号从小到到进行排序;如果是主设备号相同则按照所对应的次设备号baseminor以及minorct进即(baseminor, baseminor+minorct)进行排序,比如(1,59)排到(60,70)之前,按照块进行管理。
  • 初始化申请到的char_device_struct结构,将major、baseminor、minocrt值设置到cd指针指向的char_device_struct结构
  • 剩余部分为,将cd节点按照需要插入到cdrdev数组中对应的节点中,如果prev为空,则表明该新建的cd节点插入到cdrdev[i]数组中的首个位置。如果不为空则将cd节点插入到prev->next位置。
  • 最后处理完毕之后将chrdevs_lock锁释放。
  • out分支为出错处理,将cd释放不再插入到cdrdev数组中,跳转到out分支一般为动态分配设备号没有分配到设备号,动态申请的设备号段已经使用完毕。静态申请时 该设备号已经注册过。

chrdevs最终的排序结果可能由如下结果:

动态分配主设备号find_dynamic_major()

当major=0时,需要动态分配主设备号,最终会调用到find_dynamic_major()函数,

static int find_dynamic_major(void)
{int i;struct char_device_struct *cd;for (i = ARRAY_SIZE(chrdevs)-1; i >= CHRDEV_MAJOR_DYN_END; i--) {if (chrdevs[i] == NULL)return i;}for (i = CHRDEV_MAJOR_DYN_EXT_START;i >= CHRDEV_MAJOR_DYN_EXT_END; i--) {for (cd = chrdevs[major_to_index(i)]; cd; cd = cd->next)if (cd->major == i)break;if (cd == NULL)return i;}return -EBUSY;
}

动态申请主设备号主要主要分为两个部分:

  • 首先查看[CHRDEV_MAJOR_DYN_END ~ARRAY_SIZE(chrdevs)-1]范围段内是否使用有空闲的设备号,如果有则直接分配
  • 如果在CHRDEV_MAJOR_DYN_END~255范围未能够申请到设备号,则从[CHRDEV_MAJOR_DYN_EXT_START~CHRDEV_MAJOR_DYN_EXT_END]范围内申请设备号。

上述几个宏定义在include\linux\fs.h文件中

#define CHRDEV_MAJOR_MAX 512
/* Marks the bottom of the first segment of free char majors */
#define CHRDEV_MAJOR_DYN_END 234
/* Marks the top and bottom of the second segment of free char majors */
#define CHRDEV_MAJOR_DYN_EXT_START 511
#define CHRDEV_MAJOR_DYN_EXT_END 384

cdev结构相关接口

cdev结构

cdev结构之前已经描述过,该结构在linux-5.4.56版本中该数据结构定义在include\linux\cdev.h文件中:

struct cdev {struct kobject kobj;struct module *owner;const struct file_operations *ops;struct list_head list;dev_t dev;unsigned int count;
} __randomize_layout;

cdev_alloc申请cdev

cdev_alloc()函主要提供申请一个新的cdev结构大小内存:

struct cdev *cdev_alloc(void)
{struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);if (p) {INIT_LIST_HEAD(&p->list);kobject_init(&p->kobj, &ktype_cdev_dynamic);}return p;
}
  • 调用内核kzalloc接口,直接从slab中申请一个该结构大小的物理内存。
  • 初始化cdev结构中的list以及kobj结构,其中kobject为ktype_cdev_dynamic。

cdev_init初始化cdev

cdev_init主要是初始化cdev结构,并将file_operations *fops 赋值给cdev->ops:

void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{memset(cdev, 0, sizeof *cdev);INIT_LIST_HEAD(&cdev->list);kobject_init(&cdev->kobj, &ktype_cdev_default);cdev->ops = fops;
}

cdev_add添加cdev

cdev_add函数主要提供将cdev结构加入到系统中:

int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{int error;p->dev = dev;p->count = count;error = kobj_map(cdev_map, dev, count, NULL,exact_match, exact_lock, p);if (error)return error;kobject_get(p->kobj.parent);return 0;
}
  • 首先将要将加入的设备号以及设备数量,赋值到cdev结构中。
  • 调用kobj_map()将cdev结构添加到cdev_map中,该函数数字符设备管理核心数据稍后再详细介绍
  • 增加kob.parent增加,防止由于parent计数为0为将该cdev->kobj删除,进而引发删除cdev结构动作。

cdev_del删除cdev

cdev_del()主要提供从系统中删除cdev结构:

void cdev_del(struct cdev *p)
{cdev_unmap(p->dev, p->count);kobject_put(&p->kobj);
}
  • 调用cdev_unmap从系统中删除cdev结构
  • kobject_put减少该kobj计数,如果计数为0 则将该kobj删除。

cdev_map

cdev_map为字符设备内部管理数据,定义如下:

static struct kobj_map *cdev_map;

主要功能是提供基于kobj_map结构管理cdev, kobj_map为一个公共的数据结构,字符和块驱动管理都是使用该结构进行管理,该结构定义位于drivers\basemap\map.c文件中:

struct kobj_map {struct probe {struct probe *next;dev_t dev;unsigned long range;struct module *owner;kobj_probe_t *get;int (*lock)(dev_t, void *);void *data;} *probes[255];struct mutex *lock;
};

probes数组大小为255,采用的管理方式与chrdevs相似的方式,0、255、510...在相同的数组索引0中,故不再详细重复描述。

  • struct  probe * next :和char_device_struct *next功能类似,都是指向下一个节点为单向链表
  • dev_t dev:该probe所属设备号
  • unsigned long range: 次设备数量
  • struct module *owner:所属模块owner,内核一般使用该字段以避免在模块的操作增在被使用时卸载该模块
  • kobj_probe_t *get:为一个函数指针主要在字符驱动中主要是是获取cdev结构中的kobject
  • lock: 也是一个函数指针函数,主要是通知kobj_map模块是否将所属owner计数减一,如果没有使用则将模块ower减一,这样如果能卸载就能正常卸载。
  • data:私有数据,字符驱动中为cdev地址指针。
  • struct mutex *lock:  prble数据锁。

kobj_map_init

kobj_map_init申请并初始化kobj_map数据:

struct kobj_map *kobj_map_init(kobj_probe_t *base_probe, struct mutex *lock)
{struct kobj_map *p = kmalloc(sizeof(struct kobj_map), GFP_KERNEL);struct probe *base = kzalloc(sizeof(*base), GFP_KERNEL);int i;if ((p == NULL) || (base == NULL)) {kfree(p);kfree(base);return NULL;}base->dev = 1;base->range = ~0;base->get = base_probe;for (i = 0; i < 255; i++)p->probes[i] = base;p->lock = lock;return p;
  • 申请kobj_map和probe base内存空间
  • 初始化probe base
  • 每个probe都初始化为base,255个数组指针都有一个初始的prbe base

kobj_map

将根据dev_t和count设备数量把cdev加入到kobj_map中:


int kobj_map(struct kobj_map *domain, dev_t dev, unsigned long range,struct module *module, kobj_probe_t *probe,int (*lock)(dev_t, void *), void *data)
{unsigned n = MAJOR(dev + range - 1) - MAJOR(dev) + 1;unsigned index = MAJOR(dev);unsigned i;struct probe *p;if (n > 255)n = 255;p = kmalloc_array(n, sizeof(struct probe), GFP_KERNEL);if (p == NULL)return -ENOMEM;for (i = 0; i < n; i++, p++) {p->owner = module;p->get = probe;p->lock = lock;p->dev = dev;p->range = range;p->data = data;}mutex_lock(domain->lock);for (i = 0, p -= n; i < n; i++, p++, index++) {struct probe **s = &domain->probes[index % 255];while (*s && (*s)->range < range)s = &(*s)->next;p->next = *s;*s = p;}mutex_unlock(domain->lock);return 0;
}

该函数主要分为以下几个步骤:

  • 根据dev_t和 range 设备数量 计算有横跨了多少个主设备:unsigned n = MAJOR(dev + range - 1) - MAJOR(dev) + 1;
  • 判断n是否大于255,如果n>255,则将n=255,即一次性最多不能跨越255个主设备号
  • 申请n个probe大小内存,一个probe相当于一个主设备号。
  • 初始化申请到的probe.
  • mutex_lock(domain->lock) 对probe数据加锁
  • 将申请到的probe,按照从小到大顺序插入到主设备号对应的domain->probes[index % 255]中,其逻辑和chrdevs数组处理类似。
  • mutex_unlock(domain->lock) 释放probe 数据锁。

kobj_unmap

将设备从对应的kob_map中移除:

void kobj_unmap(struct kobj_map *domain, dev_t dev, unsigned long range)
{unsigned n = MAJOR(dev + range - 1) - MAJOR(dev) + 1;unsigned index = MAJOR(dev);unsigned i;struct probe *found = NULL;if (n > 255)n = 255;mutex_lock(domain->lock);for (i = 0; i < n; i++, index++) {struct probe **s;for (s = &domain->probes[index % 255]; *s; s = &(*s)->next) {struct probe *p = *s;if (p->dev == dev && p->range == range) {*s = p->next;if (!found)found = p;break;}}}mutex_unlock(domain->lock);kfree(found);
}
  • mutex_lock(domain->lock) 对probe数据加锁
  • 根据dev和range遍历查找到对应的probe,将查找到的probe从链表中移除
  • mutex_unlock(domain->lock) 释放probe 数据锁
  • 将查找到的节点内存释放调kfree.

kobj_lookup

根据dev_t和rang查找到从kobj_map查找到data中的kob结构:


struct kobject *kobj_lookup(struct kobj_map *domain, dev_t dev, int *index)
{struct kobject *kobj;struct probe *p;unsigned long best = ~0UL;retry:mutex_lock(domain->lock);for (p = domain->probes[MAJOR(dev) % 255]; p; p = p->next) {struct kobject *(*probe)(dev_t, int *, void *);struct module *owner;void *data;if (p->dev > dev || p->dev + p->range - 1 < dev)continue;if (p->range - 1 >= best)break;if (!try_module_get(p->owner))continue;owner = p->owner;data = p->data;probe = p->get;best = p->range - 1;*index = dev - p->dev;if (p->lock && p->lock(dev, data) < 0) {module_put(owner);continue;}mutex_unlock(domain->lock);kobj = probe(dev, index, data);/* Currently ->owner protects _only_ ->probe() itself. */module_put(owner);if (kobj)return kobj;goto retry;}mutex_unlock(domain->lock);return NULL;
}

该函数功能主要是根据dev_t和rang从kob_map中查找到kobj结构,供外面后续处理。

chrdev_open

除了上述主要函数之外char_dev还有另外一个重要的函数chrdev_open,该函数主要用于解决将之前的cdev内的file_operations 与 file联系起来,以将各种ops能够调用到具体驱动初始化所用的file_operations ops?

static int chrdev_open(struct inode *inode, struct file *filp)
{const struct file_operations *fops;struct cdev *p;struct cdev *new = NULL;int ret = 0;spin_lock(&cdev_lock);p = inode->i_cdev;if (!p) {struct kobject *kobj;int idx;spin_unlock(&cdev_lock);kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);if (!kobj)return -ENXIO;new = container_of(kobj, struct cdev, kobj);spin_lock(&cdev_lock);/* Check i_cdev again in case somebody beat us to it whilewe dropped the lock. */p = inode->i_cdev;if (!p) {inode->i_cdev = p = new;list_add(&inode->i_devices, &p->list);new = NULL;} else if (!cdev_get(p))ret = -ENXIO;} else if (!cdev_get(p))ret = -ENXIO;spin_unlock(&cdev_lock);cdev_put(new);if (ret)return ret;ret = -ENXIO;fops = fops_get(p->ops);if (!fops)goto out_cdev_put;replace_fops(filp, fops);if (filp->f_op->open) {ret = filp->f_op->open(inode, filp);if (ret)goto out_cdev_put;}return 0;out_cdev_put:cdev_put(p);return ret;
}

函数入参:

  • struct inode *inode:所操作的节点,该数据结构是内核的一个顶层设计,将各个文件按照inode节点形式一个个组织起来,后续再详细介绍。
  • struct file *filp:所操作的文件

该函数在应用层用户调用open,打开设备文件时将会调用到:

  • spin_lock(&cdev_lock); 首先对cdev加锁,后续将操作该数据
  • p = inode->i_cdev: 获取节点中要打开的cdev。
  • 如果p为空,则根据节点中要操作的inode->i_rdev设备号,从cdev_map结构中获取到对应的kobj结构。这里要特别说明是,inode->i_rdev设备号来源就是使用mknod命令将相应的/dev目录挂载到对应的设备号中。
  • new = container_of(kobj, struct cdev, kobj): 根据获取到的kobj结构获得cdev结构。
  • list_add(&inode->i_devices, &p->list):将获取到的cdev加入到inode 链表中。
  • fops = fops_get(p->ops):获取到cdev结构中的ops
  • replace_fops(filp, fops):该函数比较关键,主要是将获取到的fops 赋值到file中,后续根据操作该文件,就直接操作该file中的op,该函数定义如下:
#define replace_fops(f, fops) \do {  \struct file *__file = (f); \fops_put(__file->f_op); \BUG_ON(!(__file->f_op = (fops))); \} while(0)

通过上述代码可知,上层应用通过open函数,将驱动中的file_operations 替换到file中,后续操作就可以直接通过操作该文件来操作该设备。 

cdev_set_parent

cdev_set_parent()函数设置cdev中的kobj的父kobj:

void cdev_set_parent(struct cdev *p, struct kobject *kobj)
{WARN_ON(!kobj->state_initialized);p->kobj.parent = kobj;
}

device方式加入或者删除cdev

cdev_device_add

除了上述以指定设备号dev_t的方式将cdev加入到系统中,还支持device方式加入将cdev中,这样就能将sysfs设备模型和dev驱动模型方式结合起来:

int cdev_device_add(struct cdev *cdev, struct device *dev)
{int rc = 0;if (dev->devt) {cdev_set_parent(cdev, &dev->kobj);rc = cdev_add(cdev, dev->devt, 1);if (rc)return rc;}rc = device_add(dev);if (rc)cdev_del(cdev);return rc;
}

该函数主要是利用device中的devt设备号将其加入到系统中:

  • 首先查看dev->devt中的设备号是否为0,如果不为0,则使用首先将cdev中的kobj的父节点设置为dev->kobj
  • 然后使用ev->devt中的设备号 将cdev加入到系统中。
  • device_add(dev):将dev添加到sysfs系统中。

cdev_device_del

cdev_device_del()将cdev和dev从系统中删除:

void cdev_device_del(struct cdev *cdev, struct device *dev)
{device_del(dev);if (dev->devt)cdev_del(cdev);
}

linux 内核字符驱动char_dev源码分析相关推荐

  1. 基于Linux的UART驱动框架源码分析笔记

    文章目录 前言 一.I.MX6ULL串口接收和发送方式 1.非DMA方式 1.1.接收方式 1.2 发送方式 2.DMA方式 2.1.接收方式 2.2 发送方式 二.UART驱动注册 1.uart_r ...

  2. linux usb摄像头 源码,Linux USB摄像头驱动实现源码分析

    Spac5xx的实现是按照标准的USB VIDEO设备的驱动框架编写(其具体的驱动框架可参照/usr/src/linux/drivers/usb/usbvideo.c文件),整个源程序由四个主体部分组 ...

  3. Linux驱动修炼之道-SPI驱动框架源码分析(上)

    Linux驱动修炼之道-SPI驱动框架源码分析(上)   SPI协议是一种同步的串行数据连接标准,由摩托罗拉公司命名,可工作于全双工模式.相关通讯设备可工作于m/s模式.主设备发起数据帧,允许多个从设 ...

  4. SPI驱动框架源码分析

     SPI驱动框架源码分析 2013-04-12 16:13:08 分类: LINUX SPI驱动框架源码分析 SPI协议是一种同步的串行数据连接标准,由摩托罗拉公司命名,可工作于全双工模式.相关通讯设 ...

  5. Linux下USB suspend/resume源码分析【转】

    转自:http://blog.csdn.net/aaronychen/article/details/3928479 Linux下USB suspend/resume源码分析 Author:aaron ...

  6. Linux Kernel 2.6.9源码分析 -- send/recieve 报文

    Linux Kernel 2.6.9源码分析 – send/recieve 报文 可用户socket报文读写的函数有以下几对: ssize_t read(int fd, void *buf, size ...

  7. 详解5种红黑树的场景,从Linux内核谈到Nginx源码,听完醍醐灌顶丨Linux服务器开发丨Linux后端开发

    5种红黑树的场景,从Linux内核谈到Nginx源码,听完醍醐灌顶 1. 进程调度CFS的红黑树场景 2. 虚拟内存管理的红黑树场景 3. 共享内存slab的红黑树场景 视频讲解如下,点击观看: [干 ...

  8. Linux驱动修炼之道-SPI驱动框架源码分析(中)

    来自:http://blog.csdn.NET/woshixingaaa/article/details/6574220 这篇来分析spi子系统的建立过程. 嵌入式微处理器访问SPI设备有两种方式:使 ...

  9. linux ptrace 内核源码分析,linux 3.5.4 ptrace源码分析分析(系列一)

    ptrace是linux系统中为了调试专门设立的一种系统调用.要想调试调试一个进程,有两种方式: PTRACE_TRACEME和PTRACE_ATTACH.这两种方式的主要区别可以概括为: PTRAC ...

最新文章

  1. 基于模型的系统工程设计软件ModelCoder在航空发动机控制设计中的应用
  2. 那些炫酷的特效--QQ空间+VUE
  3. Jmeter MD5加密及其运用
  4. 双流join(SparkStreaming)
  5. Centos安装(更新)git(亲测有效)
  6. 学生选课系统—软件工程课程设计
  7. ConfigUtil.class.getResource
  8. 谷歌colab平台简单使用及读取自己的数据集
  9. Waymo无人车设立中国全资子公司,取名慧摩,与谷歌上海同楼办公
  10. PMP备考大全:经典题库(5月)
  11. “errmsg“ : “not master and slaveOk=false“_Mongo集群没有primary但有secondary时连接不上且不能读数据
  12. CTO要越过的几道坎儿
  13. ddr2的上电顺序_DDR DDR2 DDR3设计总结指导手册.pdf
  14. qt制作棋牌游戏之XO棋(井字棋)
  15. Ubuntu 常用命令大全——长期不定时更新
  16. 杭电CTF 密码学(1)
  17. 深入理解指针数组、数组指针、函数指针、函数指针数组、指向函数指针数组的指针
  18. 几个好用的windows软件推荐
  19. 使用Python制作学习英语软件
  20. PO BO VO DTO POJO DAO DO 在java中的概念

热门文章

  1. 2016年度最受欢迎开源项目,JEECG、JEEWX参与投票
  2. v$sql、v$sqlarea 、v$sqltext
  3. Eclipse4.2界面难看,启动速度,修改方法
  4. 【cGAN】conditional生成对抗网络--有代码
  5. kindeditor图片上传
  6. 深入解析React创建组件的三种方式
  7. 第六次会议(4.15)
  8. Objections vs. excuses
  9. 【Docker学习笔记(五)】小结
  10. 技术博客么?开始每天一更