Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

How to parse the Linux driver Architecture

2025-02-22 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

Shulou(Shulou.com)06/02 Report--

Today, I will talk to you about how to analyze the Linux driver architecture, which may not be well understood by many people. in order to make you understand better, the editor has summarized the following content for you. I hope you can get something according to this article.

First of all, you need to be familiar with the design and implementation of the operating system. I recommend that you read the book written by the author of MINIX and study the kernel code of MINIX. Otherwise, you don't know what modules the operating system has, what the operating system is going to do, and what functions it provides. To put it simply, the operating system should first drive CPU, then provide those major management (processes, memory, files), achieve one or two hundred system calls, provide driver interface, and switch between user mode and kernel.

Go to intel's website and look for 'Intel ®64 and IA-32 Architectures Software Developer's Manual'' to learn about CPU's architecture, working mode, and underlying coding. Otherwise, you don't know what gdt,ldt,page table, real address, protected mode, timer interrupt are, and why the operating system sets registers in this way. This block is basically full assembly language, the initialization of CPU, register settings, manuals have strict time requirements. Which operations need to mask interrupts, which need to be completed in an instruction cycle, and so on. With the above basics, you probably know what an operating system is going to do and how to drive the underlying CPU. Read linux's kernel code at this time and get twice the result with half the effort.

Kernel is divided into two modules: one is core: cpu, interrupt, process, memory management, providing system calls. The other is driver. The driver of linux is structured and does not need to start from the bottom. Various architectures are called subsystems: for example, block subsystem, net subsystem, usb subsystem, and so on. Despite the large amount of code in the operating system, driver accounts for an estimated 80% of the code, which you don't need to look at. Whether the driver is placed in the kernel or not is the difference between the micro kernel and the macro kernel.

In the process of reading the source code, just look at its outline, mainly to understand the whole structure, as well as the flow of the program. Such as: system call call, just chase one-- see how the operating system catches soft interrupts, according to the interrupt number, dispatch to the corresponding service program, how to save the scene, and how to return to the user state after completion. The core of system call call is the flow of dispatch. After chasing a system call, the others will probably know what's going on. Driver is the same, find a simple driver to see, from the driver layer to the driver architecture, the process is clear. Such as character device driver, after registration, the driver framework how to put the device into list, when there is a user request, how to find the corresponding device, call the corresponding operation function, all the way down, the process will probably be clear.

It is not recommended to read books such as linux kernel source code analysis in the first place, which will confuse readers. The correct way should be to first understand the corresponding background knowledge before reading the source code. The source code of the Driver framework is located in drivers/base/,. It is the basic framework of the entire driver pattern, which is equivalent to the Object object in the OO language. In fact, Linux driver framework is an OO structure, core module defines data structures, function interfaces, to achieve a variety of general functions-equivalent to the base class in OO. The device driver of each module only needs to implement the interface defined in the core module.

Bus, driver, device framework

The peripheral drivers of linux are managed by bus + driver + device. In fact, it is easy to understand that peripherals communicate with cpu through bus. Kernel will implement various bus specifications and device management (device detection, driver binding, etc.). The driver only needs to register its own driver to achieve read and write control of the device.

This kind of driver usually has two levels: bus subsystem + driver module, and its flow is probably as follows:

Bus_register (xx)

The bus subsystems in kernel (such as serio, usb, pci, … Will use this function to register itself

Driver_register (xx)

The driver module uses it to register itself with the bus system, so that the driver module only needs to pay attention to the implementation of the corresponding driver interface. Typically, the bus subsystem encapsulates driver_register, such as:

Serio provides serio_register_driver () usb provides usb_register_driver () pci provides pci_register_driver () registe_device (xx)

In addition to managing driver, each bus also manages device, which usually provides an API to add devices, such as input_register_device, serio_add_port. In implementation, devices are managed through a linked list, usually adding devices during initialization or probe.

Device (device) refers to the physical device that implements the bus protocol. For example, for serio bus, i8042 is one of its devices, and the device connected to the bus (mouse, keyboard) is a serio driver.

Register

Bus.c and driver.c manage bus,driver and device respectively, providing the functions of registering bus,driver and finding device.

Bus_register (* bus) this function generates two list to hold the device and driver.

INIT_LIST_HEAD (& priv- > interfaces)

Klist_init & priv- > klist_devices, klist_devices_get, klist_devices_put)

Klist_init & priv- > klist_drivers, NULL, NULL)

* priv is defined as struct subsys_private in driver/base/base.h

Driver_register (* drv) is actually a call to bus_add_driver (* drv) to add drv to klist_drivers:

Klist_add_tail (& priv- > knode_bus, & bus- > p-> klist_drivers)

Similarly, register device, also through bus_add_device (* dev), and add it to klist_devices:

Klist_add_tail (& dev- > p-> knode_bus, & bus- > p-> klist_devices)

Take hid_bus_type as an example. After executing bus_register (& hid_bus_type), the two list of hid_bus_type- > p-> klist_devices and hid_bus_type- > p-> klist_klist_drivers will be initialized in preparation for subsequent driver and device registration. The driver data structure is as follows:

Static struct hid_driver tpkbd_driver = {

.name = "lenovo_tpkbd"

.id _ table = tpkbd_devices

.input _ mapping = tpkbd_input_mapping

.probe = tpkbd_probe

.remove = tpkbd_remove

}

When registering driver, it first sets some basic parameters through _ _ hid_register_driver (& tpkbd_driver).

Hdrv- > driver.bus = & hid_bus_type

.

Driver_register (& hdrv- > driver)

After setting the 'driver.bus' field, the correspondence between driver and bus is established. Then, after driver_register, hid_bus_type- > p-> list_drivers saves the tpkbd_driver.

Q: the driver module doesn't know the data structure of hid_driver. How can it put its pointer into list?

The answer is "no". List_drivers cannot save hid_driver pointers. The driver module provides an interface: 'struct device_driver', hid_driver this structure needs to be included.

Struct hid_driver {

Const struct hid_device_id * id_table

/ * private: * /

Struct device_driver driver

}

When registering, take the address of the driver field, that is, the pointer to hid_driver.driver, driver_register (& hdrv- > driver); when callback from the driver module to the hid-core module, such as

Static int hid_bus_match (struct device * dev, struct device_driver * drv)

{

Struct hid_driver * hdrv = container_of (drv, struct hid_driver, driver)

Struct hid_device * hdev = container_of (dev, struct hid_device, dev)

Return hid_match_device (hdev, hdrv)! = NULL

}

Using container_of converts the pointer of hid_driver.driver to the pointer of hid_driver-a method similar to the use of base class pointers to derived class objects in OO programming. This method is commonly used by Linux to build frameworks.

Device and driver binding

When adding a new device, bus will follow its driver list to find a matching driver. They "match" through the id_table of device id and driver, mainly in driver_match_device () [drivers/base/base.h] let the driver determine whether the device is supported by the callback of bus- > match (). Once the match is successful, the driver field of device will be set to the corresponding driver pointer:

Really_probe ()

{

Dev- > driver = drv

If (dev- > bus- > probe) {

Ret = dev- > bus- > probe (dev)

...

} else if (drv- > probe) {

Ret = drv- > probe (dev)

...

}

}

Then callback the probe or connect function of the driver to do some initialization.

Similarly, when a new driver is added, the bus performs the same action to find the device for the driver. Therefore, binding occurs in two phases:

1: when the driver finds the device, when driver registers itself with the bus system, the function call chain is:

Driver_register-> bus_add_driver-> driver_attach () [dd.c]-- will follow the device linked list to find a matching device.

2: the device looks for the driver. When the device is added to the bus, the function call chain is:

Device_add-> bus_probe_device-> device_initial_probe-> device_attach-- will follow the driver linked list to find a matching driver.

After the match is successful, the system continues to call driver_probe_device () to callback 'drv- > probe (dev)' or 'bus- > probe (dev)-> drv- > connect (). In the probe or connect function, the driver starts the actual initialization operation. Therefore, probe () or connect () is the real driver "entry".

For driver developers, there are two basic steps:

Define device id table.probe () or connect () to start specific initialization work.

(flow chart of driver and device registration)

Example analysis: atkbd keyboard driver

Serio Bus mainly supports PS/2, serial port and other serial device protocols. Physically, i8042 control chip can be used to connect the mouse or keyboard of PS/2. Its architecture is:

Serio.c implements the bus framework. Serio_register_port registers the underlying read and write device-port is the underlying communication device of serio, which performs the underlying read and write of the serio bus. Serio_register_driver registers the driver, binds with port, and uses port for low-level read-write communication.

When registering an atkbd driver, you need to specify the port type it supports.

Serio- > id.type = SERIO_8042; / / indicates that the driver requires 8042 support.

Serio_register_driver () / / register yourself and bind the corresponding port according to the device id (8042 in this case).

As the port of serio, i8042 is registered through serio_register_port to generate serio objects, so that the driver can call i8042 through serio- > wirte/read for underlying communication. The block diagram of the data flow is as follows:

Driver matches port through bus and communicates with peripherals through port.

We can find information about devices and drivers in the sys interface [/ sys/bus/serio/] directory, which can be added through the DEVICE_ATTR_XX series macro definition. The command rules for port are serio0, serio1, and serioN are automatically added.

Dev_set_name (& serio- > dev, "serio%ld", (long) atomic_inc_return (& serio_no)-1)

After atkbd registers the serio driver, it also needs to register the input device, which needs to implement the interface of the input subsystem and work as an input device. Input.c defines the callback and the common interface, and the submodule implements the corresponding interface.

Keybord: drivers/input/keyboard/atkbd.c

Register the device

Atkbd.c: atkbd_connect → input_register_device ()

The Input subsystem communicates with the application through the event character device.

Evdev.c: evdev_init → input_register_handler →....

Cdev_init (xx, fops) / / handles the read and write operations of / dev/input/eventX.

Atkdb opens the communication link from the application to the peripheral by registering the Serio Bus driver and Input device:

Application-> / dev/input/eventX-> Input subsystem-> atkdb driver-> serio bus-> i8042 port-> physical keyboard.

We can see that character devices are mainly used to provide application layer interfaces, while the Bus framework is used to manage peripheral drivers.

Input provides proc file interface, you can view the corresponding information.

Cat / proc/bus/input/devices: you can get the event number of a device.

Cat / proc/bus/input/handlers

Through cat eventX, you can get the key to generate input_event and view the raw data in event.

Sudo cat / dev/input/eventXX | hexdump

XX: event number.

USB

Take the code of usb serial as an example to illustrate the basic workflow of usb bus driver.

Register struct usb_driver * udriver = kzalloc (sizeof (* udriver), GFP_KERNEL)

Udriver- > name = "usb_ftdi"

Udriver- > probe = usb_ftdi_probe

Rc = usb_register (udriver)

Udriver- > id_table = id_table

Rc = driver_attach (& udriver- > drvwrap.driver)

First create a udriver--

The probe function is used to initialize the hardware. Id_table is used to identify the chip types supported by the driver and to match peripherals. Usb_register registers with the usb bus driver_attach plug-in driver.

After the driver is inserted, it will be bound to the underlying device (hub) of usb. During initialization, hub will open a task to detect port changes, and use the default "endpoint" to enumerate peripherals to get its "interface descriptor". After binding with the driver, it will send the peripheral information back to the driver through the probe function. After id comparison, find the corresponding peripheral driver, and initialize the driver in the _ peripheral function.

communication

The USB device framework is device-> interface-> endpoint. If a device has both audio and storage functions, it has an "audio interface" and a "storage interface", and each interface contains several communication "endpoints" to communicate with the host. Among them, "endpoint 0" is the default communication endpoint, through which the host reads all kinds of information about the device-- called "descriptors" in the usb specification, such as: device descriptors, interface descriptors, endpoint descriptors. Each device provides a set of usb "flag descriptors" for the host to enumerate it.

_ usb_ftdi_probe_ first analyzes the "interface descriptor" to get its port information--

For (I = 0; I desc.bNumEndpoints; + + I) {

Epd = & iface_desc- > endpoint [I] .desc

If (usb_endpoint_is_bulk_in (epd)) {

...

} else if (usb_endpoint_is_bulk_out (epd)) {

...

} else if (usb_endpoint_is_int_in (epd)) {

...

} else if (usb_endpoint_is_int_out (epd)) {

...

}

}

There are four types of "endpoints" in usb--

Bulk: used for a large number of data transmission, such as: USB disk. Control: controls the transmission of information. Interrupt: low frequency and low delay data transmission. Isochronal: periodic data transmission.

The driver needs to analyze the "endpoint descriptor" in the probe function to create the corresponding endpoint, and each "endpoint" has two directions of IN/OUT--

IN: Host reads device data. OUT: Host writes data to the device.

Control pipeline

Endpoint 0 is the standard control endpoint of the usb protocol, through which the host enumerates devices and reads its information. Usb_control_msg this function reads and writes the "register" of the peripheral through the "control pipeline". The core parameters are "Request", "Request Type", "wWalue" and "wIndex". Specific parameter values need to be queried to the equipment manufacturing, if it is a general equipment, you can directly query the corresponding specifications.

Take FTDI_SIO_GET_MODEM_STATUS as an example:

/ *

* BmRequestType: 1100 0000b

* bRequest: 5

* wValue: zero

* wIndex: Port

* wLength: 1

* Data: Status

, /

Usb_control_msg (dev, usb_rcvctrlpipe (dev, 0)

5, 0xC0, 0, priv- > port

Buf, 1, WDR_TIMEOUT)

According to the definition of the device, we get the following parameters:

Request-- 6requesttype-- 0xC0wValue-- 0wIndex-- Port (found in the device descriptor)

Usb_rcvctrlpipe (dev, 0), create a read "pipe", using the default control endpoint 0.

Here is an example of setting the baud rate:

# define FTDI_SIO_SET_BAUDRATE_REQUEST_TYPE 0x40

# define FTDI_SIO_SET_BAUDRATE_REQUEST 3

/ *

* BmRequestType: 0100 0000B

* bRequest: FTDI_SIO_SET_BAUDRATE

* wValue: BaudDivisor value-see below

* wIndex: Port

* wLength: 0

* Data: None

, /

The specific calls are as follows:

Usb_control_msg (port- > serial- > dev)

Usb_sndctrlpipe (port- > serial- > dev, 0)

FTDI_SIO_SET_BAUDRATE_REQUEST

FTDI_SIO_SET_BAUDRATE_REQUEST_TYPE

1, port

NULL, 0, WDR_SHORT_TIMEOUT)

Usb_sndctrlpipe (dev, 0), create a default (endpoint 0) write control "pipe". Since only changing the command without additional data, the buf parameter is' NULL', length'0, and other core parameters can be written according to the protocol specification.

Bulk pipeline

Bulk is used for a large number of data transmission, and the data is encapsulated in urb. The full name of urb is "USB Request Block", which is similar to the IP packet of the network. It is scheduled by Host and transmitted to the device through hierarchical hub. USB2 is the one-way communication between the host and the slave, and the data is read and written by the host.

Host writes data to the device

Usb_fill_bulk_urb (port- > write_urbs [I], udev

Usb_sndbulkpipe (udev, epd- > bEndpointAddress)

Port- > bulk_out_buffers [I], buffer_size

Write_bulk_callback, port)

Usb_submit_urb (urb, mem_flags); / / start

Void write_bulk_callback (struct urb * urb) {

Count = port- > serial- > type- > prepare_write_buffer (

Port, urb- > transfer_buffer, port- > bulk_out_size)

Urb- > transfer_buffer_length = count

Result = usb_submit_urb (urb, mem_flags)

}

As shown in the sample code in the figure above, it is a process of continuously transferring data.

First, usb_fill_bulk_urb--

Usb_sndbulkpipe defines a pipe for host output. Port- > bulk_out_ buffers [I] and buffer_size provide data buffering and size. Write_bulk_callback, the callback after the urb transmission is completed.

Second, usb_submit_urb-start "write data" and submit the urb to the host for scheduling.

Finally, define the write_bulk_callback function--

Update the data buffer. Usb_submit_urb again, call the callback in turn until the data is transferred, and if the data can be transferred at once, there is no need to define the callback.

Host reads data from the device

Usb_fill_bulk_urb (port- > read_urbs [I], udev

Usb_rcvbulkpipe (udev, epd- > bEndpointAddress)

Port- > bulk_in_buffers [I], buffer_size

Type- > read_bulk_callback, port)

Usb_submit_urb (port- > read_urbs [index], mem_flags); / / start

Static void read_bulk_callback (struct urb * urb) {

For (I = 0; I actual_length; iTunes +)

Printk (KERN_CONT "0x%x", urb- > transfer_ buffer [I])

Usb_submit_urb (port- > read_urbs [urbinx], GFP_ATOMIC)

}

The above code is a continuous process of reading data.

First, usb_fill_bulk_urb--

Usb_rcvbulkpipe defines a pipe for host output. Port- > bulk_in_ buffers [I] and buffer_size provide data buffering and size. Read_bulk_callback, the callback after the urb transmission is completed.

Second, usb_submit_urb-start "read data" and submit the urb to the host for scheduling.

Finally, define the read_bulk_callback function--

Extract the data from the buffer. Usb_submit_urb again, call the callback in turn until the data has been transferred, and do not submit the urb if you no longer need to read the data.

As can be seen from the above example, the read and write process of bulk is to create urb first, then use _ usb_submit_urb_ to start reading and writing, and process data in callback. If you need to continue reading and writing, call usb_submit_urb again in the callback function.

Interrupt pipeline

The communication flow is the same as bulk, except that usb_fill_int_urb has an extra 'interval' parameter, which is used to set its scheduling time and can carry out some timing control.

Isochronal pipeline

Used to transmit data at a certain rate, such as audio and video streams. The rate is set by the 'interval' parameter, and the driver continuously writes / reads data by cycling the state of the URB.

Block equipment

Block device refers to storage device, and block device driver is storage driver such as HD,SSD. It is different from the peripheral driver, Linux uses the Block subsystem to manage them, and transforms the IO read and write requests in the application layer into Request, which is transmitted to the corresponding block device driver.

So what is the relationship between the file system and the Block subsystem?

Block subsystem mainly provides the lowest level of data reading and writing, that is, raw io, the file system uses it for IO operations, and the file system only focuses on the file system format. Take the read partition as an example: when the file system is in the initialization phase, it will call blkdev_get. If the block device has not been initialized, blkdev_get calls the block device's rescan_partitions to scan the partition and initialize the device.

Register for # define FR_MAJOR 310

# define DEVICE_NAME "fooram"

Register block device (primary device number)

Register_blkdev (FR_MAJOR, DEVICE_NAME)

Unregister_blkdev (FR_MAJOR,DEVICE_NAME)

Register the device (MAJOR, MINOR)

Blk_register_region (MKDEV (FR_MAJOR, 0), 1,....)

Blk_unregister_region (MKDEV (FR_MAJOR, 0), 1,...)

Add disk dev- > gd = alloc_disk (1)

Dev- > queue = blk_mq_init_queue (& dev → tag_set); / / initialize the request queue

Dev- > gd- > major = FR_MAJOR

Dev- > gd- > first_minor = 0

Dev- > gd- > fops = & fr_fops

Dev- > gd- > queue = dev- > queue

Dev- > gd- > private_data = dev

Sprintf (dev- > gd- > disk_name, "frd0")

Set_capacity (dev- > gd, size)

Add_disk (dev- > gd)

Initialize request queue

Struct blk_mq_tag_set tag_set

Dev- > tag_set.ops = & fr_mq_ops

Dev- > tag_set.nr_hw_queues = 1

Dev- > tag_set.queue_depth = 16

Dev- > tag_set.numa_node = NUMA_NO_NODE

Dev- > tag_set.flags = BLK_MQ_F_SHOULD_MERGE

Dev- > tag_set.driver_data = dev

Err = blk_mq_alloc_tag_set (& dev- > tag_set)

Dev- > queue = blk_mq_init_queue (& dev → tag_set)

Static struct blk_mq_ops fr_mq_ops = {

.queue _ rq = fr_queue_rq

.map _ queue = blk_mq_map_queue

}

Initializing the request queue is the core operation of adding disks, and the core of the block device driver is to expand around the "request queue". Its core task is to optimize the read and write requests of this queue. When _ add_disk_ adds the disk to the system, the request queue can be scheduled at any time.

Processing device request

* BIO data structure *

* Request Queue*

Each "struct request" of the request queue contains a bio queue, and the bio structure contains the device address (sector, length) and the corresponding memory space. The "read" request is to write the corresponding sector of the device to the corresponding memory space, while the "write" request is just the opposite, writing the data of the memory to the corresponding sector. The simplest driver is to use a loop to process each request in turn and read and write the underlying IO, such as:

* Loop processing request (fr_queue_rq) *

Rq_for_each_segment (bvec, rq, iter) {

Char * buffer = kmap_atomic (bvec.bv_page) + bvec.bv_offset

Unsigned nsecs = bvec.bv_len > > 9

Fr_transfer (dev, sector, nsecs, buffer, write)

Sector + = nsecs

Kunmap_atomic (buffer)

}

Device reads and writes data (fr_transfer)

This function is the underlying read and write operation of a specific storage device. PCI devices can use _ PCI_WRITE to write data, and USB devices can also use BULK pipes to transfer data. The following sample code has no physical device is a ram disk that simply uses memcpy_ to transfer data.

Loff_t pos = sector loff_t len = nsecs

/ / underlying IO, such as PCI_WRITE (IOBase, register add,)

If (write)

Memcpy (dev- > data + pos, buffer, len)

Else

Memcpy (buffer, dev- > data + pos, len); after reading the above, do you have any further understanding of how to parse the Linux driver architecture? If you want to know more knowledge or related content, please follow the industry information channel, thank you for your support.

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Development

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report