一、裸机驱动开发流程

​ 所谓裸机在这里主要是指系统软件平台没有用到操作系统。在基于ARM处理器平台的软件设计中,如果整个系统只需要完成一个相对简单而且独立的任务,那么可以不使用操作系统,只需要考虑在平台上如何正确地执行这个单任务程序。不过,在这种方式下同样需要一个Boot Loader,这个时候的Boot Loader一般是自己写的一个简单的启动代码加载程序。大家所熟悉的各种Boot Loader下的设备驱动,其实就是很好的裸机驱动程序。比如说U-Boot下的网卡驱动、串口驱动、LCD驱动等。
​ 在裸机方式下,ARM的软件集成开发环境就显得极为重要,因为在这种方式下可以把所有代码都放在这个环境里面编写、编译和调试。在这种方式下测试驱动程序,首先要完成CPU的初始化,然后把需要测试的程序装载到系统的RAM区/或者SDRAM中。当然,如果需要处理一些复杂的中断处理的话,最好也把CPU的复位向量表放到RAM区中。把所有程序都调试好之后,再把最后的程序烧写到Flash里面去执行。

​ 所以裸机驱动的开发相比于在Linux设备树系统下开发要麻烦的多,对技术人员要求也要高上很多,所以我也没写过什么复杂的裸机驱动,就一些简单的GPIO、中断、时钟什么的,大概流程就是:
​ 1、熟悉外设;
​ 2、使用外设所需要的引脚;
​ 3、参考开发手册配置相应的寄存器;
​ 4、驱动烧写;
​ 5、测试;

二、Linux下DTB(设备树)驱动框架

Linux 操作系统的驱动与裸机上的驱动有很多不同:

  • 具有分层结构
    1、用户程序在用户模式下,而内核、驱动在sys管理模式下,裸机是在同一个模式下运行
    2、用户程序与内核程序操纵的地址都是虚拟地址
  • 考虑多用户,并发性
    1、多个程序都在访问同一个设备(串口),它们具有互斥访问的特性,同一时间只有一个程序能占用设备
  • 考虑协议
    1、不同设备具有不同的驱动,但是协议是一成不变的,因此需要给应用程序提供一个通用的接口,这样驱动代码可以变,但协议的接口是不变的

​ Linux中的三大类驱动:字符设备驱动、块设备驱动和网络设备驱动,三类驱动的调用框架是一样的,即:应用程序对设备进行操作时,只能通过库中的函数,这个函数就会进入内核,然后内核调用驱动,驱动再操作设备。

三、字符设备驱动

​ 字符设备驱动是Linux 驱动中最基本的一类设备驱动,字符设备就是指在I/O传输过程中以字符为单位进行传输的设备,读写数据是分先后顺序的。比如GPIO、IIC、SPI,LCD等都属于字符设备。

3.1 字符驱动开发框架

  在裸机上,驱动是直接对寄存器进行操作,在Linux下也是对寄存器的操作,但是Linux下驱动的编写需要符合Linux的驱动框架,Linux驱动开发重点是了解其驱动框架。


  整个Linux驱动开发的流程框架就是:

  • 1、入口函数 module_init 挂载;
  • 2、register_chrdev 注册字符设备;
  • 3、alloc_chrdev_region 动态注册设备号;
  • 4、file_operations 设备操作函数编写(重点);
  • 5、unregister_chrdev_region 动态注销设备号;
  • 6、unregister_chrdev 注销字符设备;
  • 7、出口函数module_exit 卸载;
  • 额外的,需要指出许可MODULE_LICENSE,否则编译无法通过,作者MODULE_AUTHOR可以不写。

​ Linux内核中结构体 file_operations,集合了 Linux内核字符设备驱动操作函数,内容如下所:

struct file_operations {struct module *owner;loff_t (*llseek) (struct file *, loff_t, int);ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);int (*iterate) (struct file *, struct dir_context *);unsigned int (*poll) (struct file *, struct poll_table_struct *);long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);long (*compat_ioctl) (struct file *, unsigned int, unsigned long);int (*mmap) (struct file *, struct vm_area_struct *);int (*mremap)(struct file *, struct vm_area_struct *);int (*open) (struct inode *, struct file *);int (*flush) (struct file *, fl_owner_t id);int (*release) (struct inode *, struct file *);int (*fsync) (struct file *, loff_t, loff_t, int datasync);int (*aio_fsync) (struct kiocb *, int datasync);int (*fasync) (int, struct file *, int);int (*lock) (struct file *, int, struct file_lock *);ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);int (*check_flags)(int);int (*flock) (struct file *, int, struct file_lock *);ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);int (*setlease)(struct file *, long, struct file_lock **, void **);long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len);void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMUunsigned (*mmap_capabilities)(struct file *);
#endif
};

结构体中比较常用的几个成员:

  • owner拥有该结构体的模块的指针,一般设置为 THIS_MODULE。
  • llseek函数用于修改文件当前的读写位置。
  • read函数用于读取设备文件。
  • write函数用于向设备文件写入 (发送 )数据 。
  • poll是个轮询函数,用于查询设备是否可以进行非阻塞的读写。
  • unlocked_ioctl函数提供对于设备的控制功能,与应用程序中的 ioctl函数对应。
  • compat_ioctl函数与 unlocked_ioctl函数功能一样,区别在于在 64位系统上,32位的应用程序调用将会使用此函数。在 32位的系统上运行 32位的应用程序调用的是unlocked_ioctl。
  • mmap函数用于将将设备的内存映射到进程空间中 (也就是用户空间 ),一般帧缓冲设备会使用此函数,比如 LCD驱动的显存,将帧缓冲 (LCD显存 )映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。
  • open函数用于打开设备文件。
  • release函数用于释放 (关闭 )设备文件,与应用程序中的 close函数对应。
  • fasync函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。
  • aio_fsync函数与 fasync函数的功能类似,只是 aio_fsync是异步刷新待处理的数据。

3.2 设备树

​ Device Tree 起源于 OpenFirmware (OF),是一种采用树形结构描述硬件的数据结构,由一系列被命名的结点(node)和属性(property)组成,结点本身可包含子结点,主要由三大部分组成:头(Header)、结构块(Structure block)、字符串块(Strings block)。

  • 头:主要描述设备树的一些基本信息,例如设备树大小,结构块偏移地址,字符串块偏移地址等。偏移地址是相对于设备树头的起始地址计算的。
  • 结构块:一个线性化的结构体,是设备树的主体,以节点node的形式保存了目标单板上的设备信息。
  • 字符串块:通过节点的定义知道节点都有若干属性,而不同的节点的属性又有大量相同的属性名称,因此将这些属性名称提取出一张表,当节点需要应用某个属性名称时直接在属性名字段保存该属性名称在字符串块中的偏移量。
    设备树的编写是通过DTS文件来编写的,这里不讲DTS的语法,DTS的结构图如下:

​ 树的主干就是系统总线,IIC 控制器、GPIO 控制器、SPI 控制器等都是接到系统主线上的分支。IIC 控制器有分为IIC1 和IIC2 两种,其中IIC1 上接了FT5206 和AT24C02这两个IIC 设备,IIC2 上只接了MPU6050 这个设备。DTS 文件的主要功能就是按照图中所示的结构来描述板子上的设备信息。

3.4 驱动示例

​ 这是一个控制gpio的驱动代码,内容没什么用,用来了解一下开发流程即可,驱动代码:

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/init.h>
#include <linux/ide.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>#define CAMERA_CNT         1           // 设备号个数
#define CAMERA_NAME         "camera"  // 名字
#define TAKEPICTURE         1           // 拍照// 定义camera设备结构体
struct camera_dev{dev_t                 devid;          // 设备号struct cdev       cdev;           // cdev struct class        *class;         // 类struct device       *device;        // 设备int                major;          // 主设备号int              minor;          // 次设备号struct device_node   *nd;            // 设备节点int              focus_gpio;     // 对焦GPIO编号int                 shutter_gpio;    // 快门GPIO编号
};struct camera_dev camera;         /* 相机设备 *//*** @brief          : 打开设备* @param - inode     : 传递给驱动的inode* @param - filp   : 设备文件,file结构体有个叫做private_data的成员变量*                       一般在open的时候将private_data指向设备结构体。* @return             : 0 成功;其他 失败*/
static int camera_open(struct inode *inode, struct file *filp)
{filp->private_data = &camera; /* 设置私有数据 */return 0;
}/*** @brief           : 从设备读取数据 * @param - filp  : 要打开的设备文件(文件描述符)* @param - buf    : 返回给用户空间的数据缓冲区* @param - cnt  : 要读取的数据长度* @param - offt  : 相对于文件首地址的偏移* @return             : 读取的字节数,如果为负值,表示读取失败*/
static ssize_t camera_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{return 0;
}/*** @brief           : 向设备写数据 * @param - filp   : 设备文件,表示打开的文件描述符* @param - buf     : 要写给设备写入的数据* @param - cnt     : 要写入的数据长度* @param - offt  : 相对于文件首地址的偏移* @return             : 写入的字节数,如果为负值,表示写入失败*/
static ssize_t camera_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{int retvalue;unsigned char databuf[1];unsigned char gpiostat;struct camera_dev *dev = filp->private_data;retvalue = copy_from_user(databuf, buf, cnt);if(retvalue < 0) {printk("kernel write failed!\r\n");return -EFAULT;}gpiostat = databuf[0];       // 获取状态值if(gpiostat == TAKEPICTURE) { gpio_set_value(dev->focus_gpio, 1);      // 对焦msleep(500);gpio_set_value(dev->shutter_gpio, 1);   // 拍照msleep(500);gpio_set_value(dev->focus_gpio, 0);     // 合焦gpio_set_value(dev->shutter_gpio, 0);   // 拍照完成}else if(gpiostat == 2){gpio_set_value(dev->focus_gpio, 1);     // 合焦gpio_set_value(dev->shutter_gpio, 1);   // 拍照完成}else{gpio_set_value(dev->focus_gpio, 0);     // 合焦gpio_set_value(dev->shutter_gpio, 0);   // 拍照完成}return 0;
}/*** @brief           : 关闭/释放设备* @param - filp   : 要关闭的设备文件(文件描述符)* @return             : 0 成功;其他 失败*/
static int camera_release(struct inode *inode, struct file *filp)
{return 0;
}// 设备操作函数
static struct file_operations camera_fops = {.owner     =     THIS_MODULE,.open    =     camera_open,.read    =     camera_read,.write   =     camera_write,.release =    camera_release,
};/*** @brief      : 驱动入口函数* @param       : 无* @return       : 无*/
static int __init camera_init(void)
{int ret = 0;// 设置拍照所使用的GPIO// 1、获取设备节点:cameracamera.nd = of_find_node_by_path("/camera");if(camera.nd == NULL) {printk("camera node not find!\r\n");return -EINVAL;} else {printk("camera node find!\r\n");}// 2、 获取设备树中的gpio属性,得到相机所使用的对焦合拍照编号camera.focus_gpio = of_get_named_gpio(camera.nd, "focus-gpio", 0);camera.shutter_gpio = of_get_named_gpio(camera.nd, "shutter-gpio", 0);if(camera.focus_gpio < 0) {printk("can't get focus-gpio");return -EINVAL;}if(camera.shutter_gpio < 0) {printk("can't get shutter-gpio");return -EINVAL;}printk("focus-gpio num = %d, shutter-gpio num = %d\r\n", camera.focus_gpio, camera.shutter_gpio);// 3、设置相机引脚默认状态ret = gpio_direction_output(camera.focus_gpio, 0);ret = gpio_direction_output(camera.shutter_gpio, 0);if(ret < 0) {printk("can't set gpio!\r\n");}// 注册字符设备驱动// 1、创建设备号if (camera.major) {        //定义了设备号camera.devid = MKDEV(camera.major, 0);register_chrdev_region(camera.devid, CAMERA_CNT, CAMERA_NAME);} else {                       //没有定义设备号alloc_chrdev_region(&camera.devid, 0, CAMERA_CNT, CAMERA_NAME);    //申请设备号camera.major = MAJOR(camera.devid); //获取分配号的主设备号camera.minor = MINOR(camera.devid);    //获取分配号的次设备号}printk("camera major=%d,minor=%d\r\n",camera.major, camera.minor); // 2、初始化cdevcamera.cdev.owner = THIS_MODULE;cdev_init(&camera.cdev, &camera_fops);// 3、添加一个cdevcdev_add(&camera.cdev, camera.devid, CAMERA_CNT);// 4、创建类camera.class = class_create(THIS_MODULE, CAMERA_NAME);if (IS_ERR(camera.class)) {return PTR_ERR(camera.class);}// 5、创建设备camera.device = device_create(camera.class, NULL, camera.devid, NULL, CAMERA_NAME);if (IS_ERR(camera.device)) {return PTR_ERR(camera.device);}return 0;
}/*** @brief       : 驱动出口函数* @param       : 无* @return       : 无*/
static void __exit camera_exit(void)
{// 注销字符设备驱动cdev_del(&camera.cdev); // 删除cdevunregister_chrdev_region(camera.devid, CAMERA_CNT);    // 注销设备号device_destroy(camera.class, camera.devid);     // 注销设备class_destroy(camera.class);                 // 注销结构体
}module_init(camera_init);
module_exit(camera_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("MWBW");

​ 在写驱动的时候,有一个步骤是注册设备,这里的设备信息获取都是来自设备树,因此,驱动代码的编写只是一部分,另外还需要在对应的设备树文件中加入相应的设备节点,这个节点描述的信息要含有驱动所需要的信息,并配置引脚相应的电气属性。例如:

 camera {#address-cells = <1>;#size-cells = <1>;compatible = "jyaitech-camera";pinctrl-names = "default";pinctrl-0 = <&pinctrl_camera>;focus-gpio = <&gpio1 5 GPIO_ACTIVE_LOW>;shutter-gpio = <&gpio1 9 GPIO_ACTIVE_LOW>;status = "okay";};pinctrl_camera: cameragrp {fsl,pins = <MX6UL_PAD_GPIO1_IO05__GPIO1_IO05        0x10B0MX6UL_PAD_GPIO1_IO09__GPIO1_IO09        0x10B0>;};

3.4 驱动的分离与分层

​ 在写一些简单的字符设备驱动的时候,可以按照上面驱动,直接写死,但是面对复杂的设备,像IIC、SPI、LCD等,就应该考虑下驱动的分离与分层,这样可以极大提高驱动的可重用性。


​ 这是简单的驱动与设备的关系,是一对一的,也就是每有一个硬件就会有一个对应的设备,很明显这样很容易造成一个系统驱动垃圾堆,所以复杂的驱动一般用下面的方式:


​ 每个设备控制器都提供一个统一的接口(也叫做主机驱动),每个设备的话也只提供一个驱动程序(设备驱动),每个设备通过统一的接口驱动来访问,这样就可以大大简化驱动文件。

四、块驱动

4.1 块驱动与字符驱动区别

​ 块设备驱动块设备是针对存储设备的驱动,比如 SD卡、 EMMC、 NAND Flash、 Nor Flash、 SPI Flash、机械硬盘、固态硬盘等,驱动要远比字符设备驱动复杂得多,不同类型的存储设备又对应不同的驱动子系统。块设备驱动相比字符设备驱动的主要区别如下:

  • 块设备只能以块为单位进行读写访问,块是 linux虚拟文件系统 (VFS)基本的数据传输单位。字符设备是以字节为单位进行数据传输的,不需要缓冲。
  • 块设备在结构上是可以进行随机访问的,对于这些设备的读写都是按块进行的,块设备使用缓冲区来暂时存放数据,等到条件成熟以后在一次性将缓冲区中的数据写入块设备中。
  • 字符设备是顺序的数据流设备,字符设备是按照字节进行读写访问的。字符设备不需要缓冲区,对于字符设备的访问都是实时的,而且也不需要按照固定的块大小进行访问。
  • 块设备结构的不同其 I/O算法也会不同,比如对于 EMMC、 SD卡、 NAND Flash这类没有任何机械设备的存储设备就可以任意读写任何的扇区 (块设备物 理存储单元 )。但是对于机械硬盘这样带有磁头的设备,读取不同的盘面或者磁道里面的数据,磁头都需要进行移动,因此对于机械硬盘而言,将那些杂乱的访问按照一定的顺序进行排列可以有效提高磁盘性能, linux里面针对不同的存储设备实现了不同的 I/O调度算法。

4.2 块驱动开发框架

​ 与字符设备开发框架大致类似,区别在于:

  • 字符设备驱动我们注册的是字符设备,而块驱动注册的是块设备,注册函数:register_blkdev,注销函数:unregister_blkdev
  • 块设备需要申请gendisk并对其进行配置,gendisk是一个描述磁盘设备的结构体,具体内容如下:
struct gendisk {/* major, first_minor and minors are input parameters only,* don't use directly.  Use disk_devt() and disk_max_parts().*/int major;         /* major number of driver */int first_minor;int minors;                     /* maximum number of minors, =1 for* disks that can't be partitioned. */char disk_name[DISK_NAME_LEN];    /* name of major driver */char *(*devnode)(struct gendisk *gd, umode_t *mode);unsigned int events;      /* supported events */unsigned int async_events;    /* async events, subset of all *//* Array of pointers to partitions indexed by partno.* Protected with matching bdev lock but stat and other* non-critical accesses use RCU.  Always access through* helpers.*/struct disk_part_tbl __rcu *part_tbl;struct hd_struct part0;const struct block_device_operations *fops;struct request_queue *queue;void *private_data;int flags;struct device *driverfs_dev;  // FIXME: removestruct kobject *slave_dir;struct timer_rand_state *random;atomic_t sync_io;        /* RAID */struct disk_events *ev;
#ifdef  CONFIG_BLK_DEV_INTEGRITYstruct blk_integrity *integrity;
#endifint node_id;
};
  • 块设备结构体没有与字符设备一样的read、write这样的读写操作函数,有的操作函数如下:
struct block_device_operations {int (*open) (struct block_device *, fmode_t);void (*release) (struct gendisk *, fmode_t);int (*rw_page)(struct block_device *, sector_t, struct page *, int rw);int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);long (*direct_access)(struct block_device *, sector_t,void **, unsigned long *pfn, long size);unsigned int (*check_events) (struct gendisk *disk,unsigned int clearing);/* ->media_changed() is DEPRECATED, use ->check_events() instead */int (*media_changed) (struct gendisk *);void (*unlock_native_capacity) (struct gendisk *);int (*revalidate_disk) (struct gendisk *);int (*getgeo)(struct block_device *, struct hd_geometry *);/* this callback is with swap_lock and sometimes page table lock held */void (*swap_slot_free_notify) (struct block_device *, unsigned long);struct module *owner;
};
  • 对磁盘的配置过程:
    1、申请gendisk : struct gendisk *alloc_disk(int minors);
    2、将 gendisk 添加到内核 : void add_disk(struct gendisk *disk);
    3、设置 gendisk 容量 : void set_capacity(struct gendisk *disk, sector_t size);
    4、引用计数的调整:
    增加:truct kobject *get_disk(struct gendisk *disk)
    减少:void put_disk(struct gendisk *disk)
    5、删除gendisk: void del_gendisk(struct gendisk *gp)

4.3 块设备读写操作

​ 在上一节提到,块设备结构体没有与字符设备一样的read、write这样的读写操作函数,但它是通过请求队列 request_queue、请求 request 和 bio 结构这些操作进行读写的。

4.3.1 请求队列 request_queue

1、首先申请并初始化一个 request_queue,然后在初始化 gendisk 的时候将request_queue 地址赋值给 gendisk 的 queue 成员变量。request_queue 的申请与初始化通过使用 blk_init_queue 函数完成。
2、分配请求队列并绑定制造请求函数。blk_init_queue 函数其实以及完成了这个操作,但是面对非机械设备,先通过struct request_queue *blk_alloc_queue (gfp_t gfp_mask)函数请求设备,再通过void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn)函数绑定制造请求。
3、删除请求队列:void blk_cleanup_queue(struct request_queue *q)

4.3.2 请求request

​ 对请求的处理很简单:获取、开启、对应函数分别为:request *blk_peek_request(struct request_queue *q)、void blk_start_request(struct request *req)、与请求相关的API函数:

函数 描述
blk_end_request 请求中指定字节数据被处理完成
blk_end_request_all 请求中所有数据全部处理完成
blk_end_request_cur 当前请求中的 chunk
blk_end_request_err 处理完请求,直到下一个错误产生

4.3.3 bio结构

​ bio 是request结构体里面的一个成员,每个 request 里面里面会有多个 bio,bio 保存着最终要读写的数据、地址等信息。

五、网络驱动

5.1 嵌入式网络结构

​ 嵌入式网络硬件分为两部分:MAC 和 PHY,如果芯片支持网络,那么一般是指内部含有MAC,如果没有只能通过外置MAC,但是一般效率不高,因为内部含有MAC的芯片一般都有网络加速引擎。而 PHY一般是外置的。
​ MAC与PHY之间的接口一般是MII/RMII,它们是 IEEE-802.3 定义的以太网标准接口,连接图如下:

​ MII

​ RMII

5.2 NAPI处理机制

​ Linux 在这中断、轮询的基础上提出了另外一种网络数据接收的处理方法:NAPI(New API),NAPI 是一种高效的网络处理技术。NAPI 的核心思想就是不全部采用中断来读取网络数据,而是采用中断来唤醒数据接收服务程序,在接收服务程序中采用 POLL 的方法来轮询处理数据。这种方法的好处就是可以提高短数据包的接收效率,减少中断处理的时间。Linux 内核使用结构体 napi_struct 表示 NAPI。
​ 大致过程如下:

  • 初始化 NAPI : netif_napi_add;
  • 使能 NAPI : inline void napi_enable(struct napi_struct *n);
  • 检查 NAPI 是否可以进行调度: inline bool napi_schedule_prep(struct napi_struct *n);
  • NAPI 调度: void __napi_schedule(struct napi_struct *n);
  • NAPI 处理完成:inline void napi_complete(struct napi_struct *n);
  • 关闭NAPI:void napi_disable(struct napi_struct *n);
  • 删除 NAPI:void netif_napi_del(struct napi_struct *napi);

2021年Linux技术总结(四):Linux 驱动相关推荐

  1. Linux技术网站中文,Linux技术网站,putty工具,中文显示设置

    专业的Linux技术网站,用户遍布全国各地,拥有大批的Linux专家与工程师,汇集海量Linux信息,是中国Linux人的网上家园. 默认情况下,putty是不支持中文显示的,当使用putty ssh ...

  2. linux进程管理内存管理,Linux专业知识四:Linux系统进程管理及查看内存

    本文主讲Linux专业知识之Linux系统进程管理及查看内存的情况,以Redhat RHEL7操作系统为例. 一.进程 程序与进程:程序是静态的(文件),进程是动态的(运行的程序). 进程和线程:一个 ...

  3. linux oracle pam,Linux技术之深入Linux PAM 体系结构(一)

    本文阐述了 Linux-PAM 的概念,同时还与读者一道分析了 Linux-PAM 的体系结构,作者希望籍此以加深读者对 Linux-PAM 的理解,以便对其有更深层的把握. 一.什么是Linux-P ...

  4. 软件测试用哪个版本linux,技术|如何在 Linux 中不安装软件测试一个软件包

    出于某种原因,你可能需要在将软件包安装到你的 Linux 系统之前对其进行测试.如果是这样,你很幸运!今天,我将向你展示如何在 Linux 中使用 Nix 包管理器来实现.Nix 包管理器的一个显著特 ...

  5. Linux学习笔记(四)-Linux常用命令

    常用命令格式 #command(指令) [-options] parameter1(参数1) parameter2(参数2)... 大小写区分,tab自动补全 Shell特殊字符 通配符 管道Pipe ...

  6. 深入学习Linux摄像头(四)三星平台fimc驱动详解

    深入学习Linux摄像头系列 深入学习Linux摄像头(一)v4l2应用编程 深入学习Linux摄像头(二)v4l2驱动框架 深入学习Linux摄像头(三)虚拟摄像头驱动分析 深入学习Linux摄像头 ...

  7. Unix/Linux操作系统分析实验四 设备驱动: Linux系统下的字符设备驱动程序编程

    Unix/Linux操作系统分析实验一 进程控制与进程互斥 Unix/Linux操作系统分析实验二 内存分配与回收:Linux系统下利用链表实现动态内存分配 Unix/Linux操作系统分析实验三 文 ...

  8. Linux 串口编程四 串口设备程序开发

    Linux 串口编程和程序相对来说是很简单的,之所以用博客连载来展示,主要是想在学会使用的基础上掌握相关背景,原理以及注意事项.相信在遇到问题的时候,我们就不会对于技术的概念和 API 的使用浅尝辄止 ...

  9. linux 中级 教程pdf,Linux初中级学习者指导Linux操作系统技术合集.pdf

    红联的个人空间 Linux操作系统技术合集 作者:红联 Linux操作系统技术合 集 ─────Linux初, 中级学习者教程 Linux有些神奇,有人就这么说,Linux有些意思,我想尝尝,尚末安装 ...

最新文章

  1. 分布式存储系统的关键技术
  2. C++计算程序耗时函数用法汇总
  3. printf()用法详解(转)
  4. python就业前景-Python就业前景分析
  5. ethtool编译与内核实现介绍
  6. Linux中存储相关的命令,Linux存储管理命令与HAB相关命令
  7. android百度地图画圆,android百度地图半径画圆
  8. java 程序路径_Java程序路径
  9. 逆向分析CRACKME 第一章 Acid burn
  10. apache php 无法读取网络邻居共享文件的处理,提权
  11. A搜索算法(python)之八数码问题
  12. HTML5新特性(基本)
  13. 程序运行时的内存空间分布
  14. Python实操 :破解密码
  15. 微信群转播机器人python练习制作
  16. 学建模的快速方法【快捷键】
  17. 2022年流动式起重机司机特种作业证考试题库及答案
  18. 微软软件测试报告,windows计算器软件测试报告.doc
  19. freenom免费域名注册失败的解决办法
  20. 生活中有什么值得坚持的好习惯?

热门文章

  1. 【第十五届蓝桥杯备赛(bushi,写文凑个数)】蓝桥OJ---排列序数
  2. 【嵌入式】工程模板的编写以及工程代码的理解
  3. 柱插筋计算机械接头吗,柱基础插筋的计算.ppt
  4. vue 派送区域 地图 可自选
  5. QtCharts图形移动和缩放
  6. Ros 安装过程(ubuntu18)
  7. Memblaze PBlaze4系列PCIe闪存新品通过ESXi6.0认证
  8. 中兴网络设备交换机路由器查看所有端口流量的命令
  9. Android kernel LOGO 动画
  10. 微信 发放代金券 api