Home | Projects | Notes > Linux Device Drivers > Exercise 1-2: Pseudo Character Driver (Single Device) - Implementation
Initialize the device or make device respond to subsequent system calls such as read()
and write()
.
Detect device initialization errors
Check open permission (O_RDONLY
, O_WRONLY
, O_RDWR
)
Identify the opened device using the minor number
Prepare device private data structure if required
Update f_pos
if required
open
method is optional. If not provided, open
will always succeed and driver is not notified.
Implementation:
xxxxxxxxxx
131/**
2 * pcd_open()
3 * Desc. : Handles the open() system call from the user space
4 * Param. : @inode - pointer to inode object
5 * @filp - pointer to file object
6 * Returns : 0 on success, negative error code otherwise
7 * Note : N/A
8 */
9int pcd_open(struct inode *inode, struct file *filp)
10{
11 pr_info("open was successful\n");
12 return 0;
13}
close(fd)
system call from the user space)Does the reverse operations of open method. Simply put, release method should put the device in its default state, i.e., the state before the open method was called.
e.g., If open method brings the device out of low power mode, then release method may send the device back to the low power mode.
Free any data structures allocated by the open method.
Returns 0 on success, negative error code otherwise (e.g., the device does not respond when you try to de-initialize the device).
Implementation:
xxxxxxxxxx
151/**
2 * pcd_release()
3 * Desc. : Handles the close() system call from the user space
4 * Param. : @inode - pointer to inode struct
5 * @filp - pointer to file struct
6 * Returns : 0 on success, negative error code otherwise
7 * Note : VFS releases the file object. Called when the last reference to an
8 * open file is closed (i.e., when the f_count field of the file object
9 * becomes 0.
10 */
11int pcd_release(struct inode *inode, struct file *filp)
12{
13 pr_info("close was successful\n");
14 return 0;
15}
read(fd, buff, 20)
system call from the user space)
Read count
bytes from a device starting at position f_pos
.
Update the f_pos
by adding the number bytes successfully read.
A return value less than count
does not mean that an error has occurred.
f_op
and f_pos
Data copying (kernel space
xxxxxxxxxx
151/**
2 * copy_to_user()
3 * Desc. : Copies data from kernel space to user space (during read operation)
4 * Param. : @to - destination address in user space
5 * @from - source address in kernel space
6 * @n - number of bytes to copy
7 * Returns : 0 on success, number of bytes that could not be copied otherwise
8 * Notes : It checks whether the user space pointer @to is valid or not.
9 * If the pointer is invalid, copy will not be performed. If an invalid address
10 * is encountered during the copy, only part of the data is copied. In either case,
11 * the return value is the amount of memory left to be copied.
12 * If this function returns a non-zero value, you should assume that there was
13 * a problem during the data copy.
14 */
15unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
If
copy_to_user()
returns a non-zero value, your read function should return an appropriate error code (-EFAULT).
Implementation:
Check the user requested count
value against DEV_MEM_SIZE
of the device.
If f_pos
+ count
> DEV_MEM_SIZE
, then adjust the count
(count
= DEV_MEM_SIZE
- f_pos
).
Copy count
number of bytes from device memory to user buffer.
Update f_pos
Return number of bytes successfully read or error code
If f_pos
is at EOF, then return 0.
xxxxxxxxxx
391/**
2 * pcd_read()
3 * Desc. : Handles the read() system call from the user space
4 * Param. : @filp - pointer to file object
5 * @buff - pointer to user buffer
6 * @count - read count given by the user
7 * @f_pos - pointer to current file position from which the read has to begin
8 * Returns : The number of bytes read on success,
9 * 0 if there is no bytes to read (EOF),
10 * appropriate error code (negative value) otherwise
11 * Note : Reads a device file @count byte(s) of data from @f_pos, returns the data back to
12 * @buff (user), and updates @f_pos.
13 */
14ssize_t pcd_read(struct file *filp, char __user *buff, size_t count, loff_t *f_pos)
15{
16 pr_info("Read requested for %zu bytes\n", count);
17 pr_info("Current file position = %lld\n", *f_pos);
18
19 /* Adjust the count */
20 if ((*(f_pos) + count) > DEV_MEM_SIZE)
21 count = DEV_MEM_SIZE - *f_pos;
22
23 /* Copy to user buffer
24 Note: Global data access should be synchronized by using mutual exclusion to avoid
25 race condition. */
26 if (copy_to_user(buff, &device_buffer[*f_pos], count))
27 {
28 return -EFAULT;
29 }
30
31 /* Update the current file position f_pos */
32 *f_pos += count;
33
34 pr_info("%zu bytes successfully read\n", count);
35 pr_info("Updated file position = %lld\n", *f_pos);
36
37 /* Return the number of bytes successfully read */
38 return count;
39}
__user
Optional macro that alerts the programmer that this is a user level pointer so cannot be trusted for direct dereferencing.
A macro used with user level pointers which tells the developer not to trust or assume it as a valid pointer to avoid kernel faults.
Never try to dereference user given pointers directly in kernel level programming. Instead, use dedicated kernel functions such as
copy_to_user
andcopy_from_user
.GCC doesn't care whether you use
__user
macro with user level pointer or not. This is checked bysparse
, a semantic checker tool of Linux kernel to find possible coding faults.
write(fd, buff, 20)
)
Write count
bytes into the device starting at position f_pos
.
Update the f_pos
by adding the number of bytes successfully written
Data copying (user space
xxxxxxxxxx
151/**
2 * copy_from_user()
3 * Desc. : Copies data from user space to kernel space (during write operation)
4 * Param. : @to - destination address in user space
5 * @from - source address in kernel space
6 * @n - number of bytes to copy
7 * Returns : 0 on success, number of bytes that could not be copied otherwise
8 * Notes : It checks whether the user space pointer @to is valid or not.
9 * If the pointer is invalid, copy will not be performed. If an invalid address
10 * is encountered during the copy, only part of the data is copied. In either case,
11 * the return value is the amount of memory left to be copied.
12 * If this function returns a non-zero value, you should assume that there was
13 * a problem during the data copy.
14 */
15unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
If
copy_from_user()
returns a non-zero value, your write function should return an appropriate error code (-EFAULT).
Implementation:
Check the user requested count
value against DEV_MEM_SIZE
of the device
If f_pos
+ count
> DEV_MEM_SIZE
, then adjust the count
(count
= DEV_MEM_SIZE
- f_pos
).
Copy count
number of bytes from user buffer to device memory
Update f_pos
Return the number of bytes successfully written or error code
xxxxxxxxxx
441/**
2 * pcd_write()
3 * Desc. : Handles the write() system call from the user space
4 * Param. : @filp - pointer to file object
5 * @buff - pointer to user buffer
6 * @count - read count given by the user
7 * @f_pos - pointer to current file position from which the read has to begin
8 * Returns : The number of bytes written on success,
9 * appropriate error code (negative value) otherwise
10 * Note : Writes a device file @count byte(s) of data from @f_pos, returns the data back to
11 * @buff (user) and updates @f_pos.
12 */
13ssize_t pcd_write(struct file *filp, const char __user *buff, size_t count, loff_t *f_pos)
14{
15 pr_info("Write requested for %zu bytes\n", count);
16 pr_info("Current file position = %lld\n", *f_pos);
17
18 /* Adjust the count */
19 if ((*(f_pos) + count) > DEV_MEM_SIZE)
20 count = DEV_MEM_SIZE - *f_pos;
21
22 if (!count)
23 {
24 pr_err("No more space left on the device\n");
25 return -ENOMEM;
26 }
27
28 /* Copy from user buffer
29 Note: Global data access should be synchronized by using mutual exclusion to avoid
30 race condition. */
31 if (copy_from_user(&device_buffer[*f_pos], buff, count))
32 {
33 return -EFAULT;
34 }
35
36 /* Update the current file position f_pos */
37 *f_pos += count;
38
39 pr_info("%zu bytes successfully written\n", count);
40 pr_info("Updated file position = %lld\n", *f_pos);
41
42 /* Return the number of bytes successfully written */
43 return count;
44}
llseek(fd, buff, 20)
)Used to alter the f_pos
.
If whence
= SEEK_SET
, filp->f_pos
= off
If whence
= SEEK_CUR
, filp->f_pos
= filp->f_pos + off
If whence
= SEEK_END
, filp->f_pos
= DEV_MEM_SIZE + off
Implementation:
xxxxxxxxxx
471/**
2 * pcd_lseek()
3 * Desc. : Handles the llseek() system call
4 * Param. : @filp - pointer to file object
5 * @offset - offset value
6 * @whence - origin
7 * - SEEK_SET: The file offset is set to @offset bytes
8 * - SEEK_CUR: The file offset is set to its current location plus @offset bytes
9 * - SEEK_END: The file offset is set to the size of the file plus @offset bytes
10 * Returns : Newly updated file position on sucess, error code other wise
11 * Note : Updates the file pointer by using @offset and @whence information.
12 * Allows the file offset to be set beyond the end of the file (but this does not
13 * change the size of the file).
14 */
15loff_t pcd_lseek(struct file *filp, loff_t offset, int whence)
16{
17 loff_t temp;
18
19 pr_info("lseek requested\n");
20 pr_info("Current file position = %lld\n", filp->f_pos);
21
22 switch (whence)
23 {
24 case SEEK_SET:
25 if ((offset > DEV_MEM_SIZE) || (offset < 0))
26 return -EINVAL;
27 filp->f_pos = offset;
28 break;
29 case SEEK_CUR:
30 temp = filp->f_pos + offset;
31 if ((temp > DEV_MEM_SIZE) || (temp < 0))
32 return -EINVAL;
33 filp->f_pos = temp;
34 break;
35 case SEEK_END:
36 temp = DEV_MEM_SIZE + offset;
37 if ((temp > DEV_MEM_SIZE) || (temp < 0))
38 return -EINVAL;
39 filp->f_pos = temp;
40 break;
41 default:
42 return -EINVAL;
43 }
44
45 pr_info("Updated file position = %lld\n", filp->f_pos);
46 return filp->f_pos;
47}
xxxxxxxxxx
3011/**
2 * Filename : pcd.c
3 * Description : A pseudo character deriver
4 * Author : Kyungjae Lee
5 * Created : 05/19/2023
6 * Updates : 06/01/2023 - Complete file operation methods implementation
7 * - Add error handling features
8 */
9
10
11
12
13/* class_create(), device_create() */
14/* MAJOR(), MINOR() */
15
16
17
18
19/* For debugging purpose, prefix current function name in front of the string printed by pr_info() */
20
21/* Global variables */
22char device_buffer[DEV_MEM_SIZE]; /* Pseudo character device's buffer */
23dev_t device_number; /* Stores the device number */
24struct cdev pcd_cdev; /* Cdev variable */
25
26/**
27 * Device driver specific file operation methods that handle system calls
28 */
29
30/**
31 * pcd_lseek()
32 * Desc. : Handles the llseek() system call
33 * Param. : @filp - pointer to file object
34 * @offset - offset value
35 * @whence - origin
36 * - SEEK_SET: The file offset is set to @offset bytes
37 * - SEEK_CUR: The file offset is set to its current location plus @offset bytes
38 * - SEEK_END: The file offset is set to the size of the file plus @offset bytes
39 * Returns : Newly updated file position on sucess, error code other wise
40 * Note : Updates the file pointer by using @offset and @whence information.
41 * Allows the file offset to be set beyond the end of the file (but this does not
42 * change the size of the file).
43 */
44loff_t pcd_lseek(struct file *filp, loff_t offset, int whence)
45{
46 loff_t temp;
47
48 pr_info("lseek requested\n");
49 pr_info("Current file position = %lld\n", filp->f_pos);
50
51 switch (whence)
52 {
53 case SEEK_SET:
54 if ((offset > DEV_MEM_SIZE) || (offset < 0))
55 return -EINVAL;
56 filp->f_pos = offset;
57 break;
58 case SEEK_CUR:
59 temp = filp->f_pos + offset;
60 if ((temp > DEV_MEM_SIZE) || (temp < 0))
61 return -EINVAL;
62 filp->f_pos = temp;
63 break;
64 case SEEK_END:
65 temp = DEV_MEM_SIZE + offset;
66 if ((temp > DEV_MEM_SIZE) || (temp < 0))
67 return -EINVAL;
68 filp->f_pos = temp;
69 break;
70 default:
71 return -EINVAL;
72 }
73
74 pr_info("Updated file position = %lld\n", filp->f_pos);
75 return filp->f_pos;
76}
77
78/**
79 * pcd_read()
80 * Desc. : Handles the read() system call from the user space
81 * Param. : @filp - pointer to file object
82 * @buff - pointer to user buffer
83 * @count - read count given by the user
84 * @f_pos - pointer to current file position from which the read has to begin
85 * Returns : The number of bytes read on success,
86 * 0 if there is no bytes to read (EOF),
87 * appropriate error code (negative value) otherwise
88 * Note : Reads a device file @count byte(s) of data from @f_pos, returns the data back to
89 * @buff (user), and updates @f_pos.
90 */
91ssize_t pcd_read(struct file *filp, char __user *buff, size_t count, loff_t *f_pos)
92{
93 pr_info("Read requested for %zu bytes\n", count);
94 pr_info("Current file position = %lld\n", *f_pos);
95
96 /* Adjust the count */
97 if ((*(f_pos) + count) > DEV_MEM_SIZE)
98 count = DEV_MEM_SIZE - *f_pos;
99
100 /* Copy to user buffer
101 Note: Global data access should be synchronized by using mutual exclusion to avoid
102 race condition. */
103 if (copy_to_user(buff, &device_buffer[*f_pos], count))
104 {
105 return -EFAULT;
106 }
107
108 /* Update the current file position f_pos */
109 *f_pos += count;
110
111 pr_info("%zu bytes successfully read\n", count);
112 pr_info("Updated file position = %lld\n", *f_pos);
113
114 /* Return the number of bytes successfully read */
115 return count;
116}
117
118/**
119 * pcd_write()
120 * Desc. : Handles the write() system call from the user space
121 * Param. : @filp - pointer to file object
122 * @buff - pointer to user buffer
123 * @count - read count given by the user
124 * @f_pos - pointer to current file position from which the read has to begin
125 * Returns : The number of bytes written on success,
126 * appropriate error code (negative value) otherwise
127 * Note : Writes a device file @count byte(s) of data from @f_pos, returns the data back to
128 * @buff (user) and updates @f_pos.
129 */
130ssize_t pcd_write(struct file *filp, const char __user *buff, size_t count, loff_t *f_pos)
131{
132 pr_info("Write requested for %zu bytes\n", count);
133 pr_info("Current file position = %lld\n", *f_pos);
134
135 /* Adjust the count */
136 if ((*(f_pos) + count) > DEV_MEM_SIZE)
137 count = DEV_MEM_SIZE - *f_pos;
138
139 if (!count)
140 {
141 pr_err("No more space left on the device\n");
142 return -ENOMEM;
143 }
144
145 /* Copy from user buffer
146 Note: Global data access should be synchronized by using mutual exclusion to avoid
147 race condition. */
148 if (copy_from_user(&device_buffer[*f_pos], buff, count))
149 {
150 return -EFAULT;
151 }
152
153 /* Update the current file position f_pos */
154 *f_pos += count;
155
156 pr_info("%zu bytes successfully written\n", count);
157 pr_info("Updated file position = %lld\n", *f_pos);
158
159 /* Return the number of bytes successfully written */
160 return count;
161}
162
163/**
164 * pcd_open()
165 * Desc. : Handles the open() system call from the user space
166 * Param. : @inode - pointer to inode object
167 * @filp - pointer to file object
168 * Returns : 0 on success, negative error code otherwise
169 * Note : N/A
170 */
171int pcd_open(struct inode *inode, struct file *filp)
172{
173 pr_info("open was successful\n");
174
175 return 0;
176}
177
178/**
179 * pcd_release()
180 * Desc. : Handles the close() system call from the user space
181 * Param. : @inode - pointer to inode object
182 * @filp - pointer to file object
183 * Returns : 0 on success, negative error code otherwise
184 * Note : VFS releases the file object. Called when the last reference to an
185 * open file is closed (i.e., when the f_count field of the file object
186 * becomes 0.
187 */
188int pcd_release(struct inode *inode, struct file *filp)
189{
190 pr_info("release was successful\n");
191
192 return 0;
193}
194
195/* File operations of the driver */
196struct file_operations pcd_fops = {
197 /* This struct member init method is supported from C99 on */
198 .open = pcd_open,
199 .write = pcd_write,
200 .read = pcd_read,
201 .llseek = pcd_lseek,
202 .release = pcd_release,
203 .owner = THIS_MODULE
204};
205
206struct class *class_pcd;
207struct device *device_pcd;
208
209/* Module initialization entry point */
210static int __init pcd_driver_init(void)
211{
212 int ret;
213
214 /* 1. Dynamically allocate a device number */
215 ret = alloc_chrdev_region(&device_number, 0, 1, "pcd_devices"); /* fs/char_dev.c */
216 /* Handle error */
217 if (ret < 0)
218 {
219 pr_err("Device number allocation (alloc_chrdev_region) failed\n");
220 goto err_alloc_chrdev_region;
221 }
222
223 pr_info("Device number <major>:<minor> = %d:%d\n",MAJOR(device_number), MINOR(device_number));
224 /* Print the function name for debugging purpose */
225
226 /* 2. Initialize the cdev structure with fops */
227 cdev_init(&pcd_cdev, &pcd_fops);
228
229 /* 3. Register a device (cdev structure) with VFS */
230 pcd_cdev.owner = THIS_MODULE;
231 ret = cdev_add(&pcd_cdev, device_number, 1); /* Register one device */
232 /* Handle error */
233 if (ret < 0)
234 {
235 pr_err("Device registration (cdev_add) failed\n");
236 goto err_cdev_add;
237 }
238
239 /* 4. Create device class under /sys/class/ */
240 class_pcd = class_create(THIS_MODULE, "pcd_class"); /* Returns a pointer to class struct */
241 if (IS_ERR(class_pcd))
242 {
243 pr_err("Class creation (class_create) failed\n");
244 ret = PTR_ERR(class_pcd);
245 /* PTR_ERR() converts pointer to error code (int)
246 ERR_PTR() converts error code (int) to pointer */
247 goto err_class_create;
248 }
249
250 /* 5. Populate the sysfs with device information */
251 device_pcd = device_create(class_pcd, NULL, device_number, NULL, "pcd");
252 /* Second arg is NULL since there's no hierarchy at this point */
253 /* "pdc" will appear under /dev/ */
254 if (IS_ERR(device_pcd))
255 {
256 pr_err("Device creation (device_create) failed\n");
257 ret = PTR_ERR(device_pcd);
258 goto err_device_create;
259 }
260
261 pr_info("Module init was successful\n");
262
263 return 0;
264
265/* Handle when device_create() function fails */
266err_device_create:
267 class_destroy(class_pcd);
268
269/* Handle when class_create() function fails */
270err_class_create:
271 cdev_del(&pcd_cdev);
272
273/* Handle when cdev_add() function fails */
274err_cdev_add:
275 unregister_chrdev_region(device_number, 1);
276
277/* Handle when alloc_chrdev_region() function fails */
278err_alloc_chrdev_region:
279 pr_info("Module insertion failed\n");
280 return ret; /* returns the error code */
281}
282
283/* Module cleanup entry point */
284static void __exit pcd_driver_cleanup(void)
285{
286 device_destroy(class_pcd, device_number);
287 class_destroy(class_pcd);
288 cdev_del(&pcd_cdev);
289 unregister_chrdev_region(device_number, 1);
290 pr_info("module unloaded\n");
291}
292
293/* Registration */
294module_init(pcd_driver_init);
295module_exit(pcd_driver_cleanup);
296
297/* Module description */
298MODULE_LICENSE("GPL"); /* This module adheres to the GPL licensing */
299MODULE_AUTHOR("Kyungjae Lee");
300MODULE_DESCRIPTION("A pseudo character driver");
301MODULE_INFO(board, "BeagleBone Black REV A5");