Timeline
Timeline
2026-06-29
init
This article introduces the implementation mechanism of the Rpmsg Char Driver character device driver based on Linux 5.10.x. It discusses in detail its classic architecture design that separates control devices from endpoint devices, and summarizes the specific processes and key code logic of driver registration, deferred endpoint creation, lock and wait queue synchronization mechanisms, and user-space message sending and receiving.
The current code analysis is based on linux 5.10.x

rpmsg_char.cimplementsrpmsg_driverand registered to the Rpmsg Bus, providing an interface for user-space interaction, and taking overvirtio_rpmsg_bus.cregisteredrpmsg_device
module_init / module_exit
1 |
|
rpmsg_char.cis a character device driver, with the following hierarchy:

Note that it usespostcore_initcall, which is moremodule_initearlier, because rpmsg_core.c's bus also usespostcore_initcall, and both are registered after the driver model is ready.
rpmsg_char_initdoes three things:
alloc_chrdev_region(&rpmsg_major, 0, RPMSG_DEV_MAX, "rpmsg")—— Dynamically allocate the major device number,RPMSG_DEV_MAX = MINORMASK+1(i.e., the entire minor device number space).class_create(THIS_MODULE, "rpmsg")—— Create/sys/class/rpmsg, so thatcdev_device_addit will automatically/dev/generate device nodes under (relying onudev/devtmpfs)。register_rpmsg_driver(&rpmsg_chrdev_driver)—— Register an rpmsg driver (not a platform driver! It is a driver attached to the rpmsg bus).
Andrpmsg_chrdev_driverAs follows:
1 | static struct rpmsg_driver rpmsg_chrdev_driver = { |
Key data structures
| Type | Device node | struct | Created by | Function |
|---|---|---|---|---|
| Control device | /dev/rpmsg_ctrl0 | rpmsg_ctrldev | rpmsg_chrdev_probe | One per channel; users “create endpoints” on it via ioctl |
| Endpoint device | /dev/rpmsg0 | rpmsg_eptdev | rpmsg_ctrldev_ioctl(CREATE) | One per endpoint; users read/write/poll it to send and receive messages |
This separation of “control device + data device” is a very classic pattern in Linux:The control node is used to instantiate the data node, and the data node carries the actual I/O。
struct rpmsg_ctrldev
1 | /** |
struct rpmsg_eptdev
1 | /** |
Locks and wait queues:
struct mutex ept_lock;: Protects the ept pointer. ept becomes NULL in three cases:release、destroy ioctl, parent deviceremove. The read/write path must acquire this lock first to confirm the ept is still alive.spinlock_t queue_lock;: Protects the queue (skb linked list). The callbackrpmsg_ept_cbenqueues in interrupt/atomic context, read_iter dequeues in process context, so a spinlock is used here and in the cbspin_lock, and in readspin_lock_irqsave。wait_queue_head_t readq;:rpmsg_ept_cbAfter enqueuing,wake_up_interruptible,read_iterWhen the queue is empty,wait_event_interruptible. This is a typical “producer-consumer + wait queue”.
rpmsg_driver
1 | static struct rpmsg_driver rpmsg_chrdev_driver = { |
Note: No callback function is defined here
rpmsg_chrdev_probe()
1 |
|
When the rpmsg bus matches arpmsg_devicetorpmsg_chrdev_driverit is calledrpmsg_chrdev_probe. Process:
- kzalloc a ctrldev,
ctrldev->rpdev = rpdev。 device_initialize+ setparent/class,cdev_init(&ctrldev->cdev, &rpmsg_ctrldev_fops)。- twice
ida_simple_get: fromrpmsg_minor_idaget the minor device number, fromrpmsg_ctrl_idaget id (used forrpmsg_ctrl%d)。 cdev_device_add—— register cdev and device together,/dev/rpmsg_ctrlNappears.- Key point:
dev->release = rpmsg_ctrldev_release_deviceis in thecdev_device_addis assigned afterwards. The comment says “We can now rely on the release function for cleanup”——meaningcdev_device_addIf it fails before success, it goes to the manual error label for release; after success, it is handed over to the device model’s release callback for cleanup (kfree when the last reference drops to zero). This is a very standard refcount + release paradigm in the Linux device model. dev_set_drvdata(&rpdev->dev, ctrldev)—— so that during remove, ctrldev can be retrieved from rpdev.
rpmsg_chrdev_remove()
1 | static void rpmsg_chrdev_remove(struct rpmsg_device *rpdev) |
rpmsg_ctrldev control node
file_operations
1 | static const struct file_operations rpmsg_ctrldev_fops = { |
rpmsg_ctrldev_open()
1 | static int rpmsg_ctrldev_open(struct inode *inode, struct file *filp) |
rpmsg_ctrldev_release()
1 | static int rpmsg_ctrldev_release(struct inode *inode, struct file *filp) |
In the probe function there isdev->release = rpmsg_ctrldev_release_device;, therefore the actualreleasefunction is
1 | static void rpmsg_ctrldev_release_device(struct device *dev) |
rpmsg_ctrldev_ioctl()
1 | static long rpmsg_ctrldev_ioctl(struct file *fp, unsigned int cmd, |
create ept through this function, callingrpmsg_eptdev_create()
rpmsg_eptdev_create()
1 | static int rpmsg_eptdev_create(struct rpmsg_ctrldev *ctrldev, |
- kzalloc eptdev, fill in rpdev and chinfo.
- Initialize lock, skb queue, and wait queue head.
- device_initialize + set
class/parent/groups/drvdata。groups = rpmsg_eptdev_groupsprovide name/src/dst read-only attributes for sysfs - cdev_init uses
rpmsg_eptdev_fops。 - ida twice_simple_get to fetch minor and ept id,
dev_set_name(dev, "rpmsg%d", ret)→/dev/rpmsgN。 - cdev_device_add, set dev->release upon success.
- Note: At this point,
eptdev->ept == NULL! The real endpoint hasn’t been created yet.The endpoint is created only upon open. This is lazy creation to avoid holding an address without anyone using it.
The goto order in the error handling path is strictly the reverse of the resource acquisition order, which is the standard unwind style. Note the free_eptdev’s put_device followed by kfree—there’s actually a detail here:device_initializeAfterwards, one should use put_device to let the release callback free it, but because release is set only after cdev_device_add succeeds, it hasn’t been set in this path, so put_device won’t actually kfree, a manual kfree is required. This is exactly the cost of the delayed assignment of release.
compat_ptr_ioctl()
1 |
|
Directly usefs/ioctl.cin thecompat_ptr_ioctlimplementation, 64-bit kernel compatible with 32-bit userspace
rpmsg_eptdev ept node
file_operations
1 | static const struct file_operations rpmsg_eptdev_fops = { |
rpmsg_eptdev_open()
1 | static int rpmsg_eptdev_open(struct inode *inode, struct file *filp) |
rpmsg_create_eptforwarded to the core and then to the virtio backendvirtio_rpmsg_create_ept→__rpmsg_create_ept(virtio_rpmsg_bus.c:). The latter allocates a local address in the idr (ifchinfo.src == RPMSG_ADDR_ANYthen dynamically allocate starting from 1024), binding the callbackrpmsg_ept_cbandpriv=eptdevto that address.- After the callback is registered, messages sent from the remote with a dst equal to this local address will trigger
rpmsg_ept_cb(see backendrpmsg_recv_singlelook up bymsg->dstin the idr and callept->cb)。 get_device/ open failureput_device: ensuring the device is not released while the file is open.
And the ept’s callback is as follows:
1 | static int rpmsg_ept_cb(struct rpmsg_device *rpdev, void *buf, int len, |
- The backend calls ept->cb in the virtqueue callback (softirq context), so mutexes cannot be used here, cannot sleep, use GFP_ATOMIC and spinlocks.
- A memory copy is performed here (buf → skb). The backend receives the vring buffer via zero-copy, but to hand the data over to userspace and prevent the buffer from being held by the user for a long time (the backend needs to return the buffer to the rvq as soon as possible), a copy is made into the skb here.
rpmsg_recv_singleImmediately after calling the cbvirtqueue_add_inbufreturn the original buffer.
rpmsg_eptdev_release()
1 | static int rpmsg_eptdev_release(struct inode *inode, struct file *filp) |
Actually callsdev->release
1 | static int rpmsg_eptdev_destroy(struct device *dev, void *data) |
rpmsg_eptdev_read_iter()
1 | static ssize_t rpmsg_eptdev_read_iter(struct kiocb *iocb, struct iov_iter *to) |
- The waiting condition also checks
!ept: This way,when destroy sets ept to NULL and calls wake_up, the blocked read can wake up immediately and return -EPIPE, instead of getting stuck forever. This is the standard way to write it, using “resource disappearance” as the wake-up condition. - One read retrieves one message: rpmsg is a protocol with clear message boundaries,one skb = one rpmsg message。
use = min_t(size_t, iov_iter_count(to), skb->len);which means if the user buffer is smaller than the message, the excess part is discarded (the skb is freed directly). This is a pitfall for beginners: the buffer must be large enough (the backend buffer is 512 bytes minus 16 bytes header = 496 bytes payload). - Use
spin_lock_irqsaverather thanspin_lock: Because the read might be in a normal process context, but the lock might be held by a cb in an interrupt context, disabling interrupts is safer.
rpmsg_eptdev_write_iter()
1 | static ssize_t rpmsg_eptdev_write_iter(struct kiocb *iocb, |
first
copy_from_iterAcquire the lock again, becausethe userspace copy might page fault and sleep, and should not be done while holding ept_lock — otherwise it would block the destroy path for a long time.rpmsg_sendThe blocking version will go all the way to the backendrpmsg_send_offchannel_raw(..., wait=true): if there is no TX buffer, it willwait_event_interruptible_timeout(..., 15000)。rpmsg_trysendiswait=false, if there is no buffer, it immediately-ENOMEM。successfully returns len (number of bytes written), not ret (the backend ret is 0). Note that a single message is limited by the backend buffer:
len > buf_size - sizeof(hdr)meaning 496 bytes will-EMSGSIZE。Sending is also one write = one message, there is no streaming concatenation.
rpmsg_eptdev_poll()
1 | static __poll_t rpmsg_eptdev_poll(struct file *filp, poll_table *wait) |
rpmsg_eptdev_ioctl()
1 | static long rpmsg_eptdev_ioctl(struct file *fp, unsigned int cmd, |
rpmsg_poll forwards to the backend’sops->poll— but note that the virtio backend’svirtio_endpoint_opsdoes not implement poll, sorpmsg_pollinif (!ept->ops->poll) return 0, meaning that under this backend, poll only reports readable, not writable. This is an optional backend implementation.
