Linux device driver learning notes - Advanced Character driver operation chapter - Note.3[ioctl interface]

Linux device driver learning notes - Advanced Character driver operation chapter - Note.3[ioctl interface]

When transmitting data between the application layer and the kernel layer, for example, if only 1 and 0 are used to control the operation of LED lights on and off, there are many unstable factors. One byte of data may be changed or lost by the interference generated by the environment, resulting in a series of misoperations or no response, which is also not conducive to the readability and maintainability of the code. The linux kernel is specifically for small data (one byte) provides a special data interaction interface IOCTL -- > input output control -- > input output control -- > IOCTL provides a special interface for interactive use for registers or 32-bit gpio ports. The resulting 0 or 1 is not chaotic and uniquely identifies a device

6.1 ioctl interface

In user space, ioctl system calls have the following prototypes:

int ioctl(int fd, unsigned long cmd, ...);

The point in the prototype does not represent the parameter of a variable, but a single optional parameter, which is traditionally identified as char *argp
These points are there only to prevent type checking at compile time. The actual characteristics of the third parameter depend on the specific control command issued (the second parameter)
Some commands use no parameters, some use an integer value, and some use pointers to other data. Using a pointer is a method that passes arbitrary data to ioctl calls;
The device can then exchange any amount of data with the user space

Each ioctl command, basically, is a separate, often undocumented system call, and there is no way to verify these calls in any kind of comprehensive way
It is also difficult to make unstructured ioctl parameters work consistently on all systems; for example, consider a 64 bit system running a user process in 32-bit mode
As a result, there is great pressure to achieve hybrid control operations only by any other method
Possible options include embedding commands into the data stream or using a virtual file system, either sysfs or a device specific file system

Prototypes in the file_operations structure in kernel space:

long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);

The ioctl driving method has different prototypes from the user space version:

int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd,	unsigned long arg);
		|-->inode and filp The pointer is the file descriptor passed by the corresponding application fd Value of, And passed to open Method. 
		|-->cmd Parameters are passed unchanged from the user
		|-->Optional parameters arg Parameter with a unsigned long Formal transmission of, Whether or not it is given as an integer or a pointer by the user. 
			If the caller does not pass the third parameter, it is received by the driver operation arg The value is undefined. 
			Because type checking is turned off on this extra parameter, The compiler cannot warn you if an invalid parameter is passed to ioctl, And any associated errors will be difficult to find.

As you might think, most ioctl implementations include a large switch statement to select the correct method according to the cmd parameters. Different commands have different values,
They are often given symbol names to simplify coding. Symbol names are arranged through a preprocessing definition. Custom drivers often declare such symbols in their header files

6.1.1 select ioctl command

Before writing code for ioctl, you need to select the corresponding command number. The ioctl command number should be unique in this system,
An error caused to prevent sending the correct command to the wrong device
A FIFO or an audio device. If such ioctl number is unique, the application gets an EINVAL error instead of continuing to do what should not be done

The correct way to define the ioctl command number uses four bit segments, which have the following meanings. The new symbols introduced in this list are defined in < Linux / ioctl. H >

Use a 32-bit unsigned int cmd To control
	bits    meaning
	31-30  00 - no parameters: uses _IO macro   --> It means that there is no variable parameter
	10 - read: _IOR                      --> read IO
	01 - write: _IOW                     --> write IO
	11 - read/write: _IOWR               --> Reading and writing IO
	
	29-16  size of arguments                    --> The size of the memory pointed to by the variable parameter
	
	15-8   ascii character supposedly           --> Type of specific operation mask --> 256 Type
	unique to each driver
	
	7-0    function #                           -->Specific operation mask functions -- > 256 functions
	Change through different bits,Parameter different operation mask

ioctl Explanation in header file
/* ioctl command encoding: 32 bits total, command in lower 16 bits,
 * size of the parameter structure in the lower 14 bits of the
 * upper 16 bits.
 * Encoding the size of the parameter structure in the ioctl request
 * The highest 2 bits are reserved for indicating the ``access mode''.
 * NOTE: This limits the max parameter size to 16kB -1 !
 */

#define _IOC_NRBITS     8
#define _IOC_TYPEBITS   8
#define _IOC_SIZEBITS   14
#define _IOC_DIRBITS    2

#define _IOC_NRMASK     ((1 << _IOC_NRBITS)-1)
#define _IOC_TYPEMASK   ((1 << _IOC_TYPEBITS)-1)
#define _IOC_SIZEMASK   ((1 << _IOC_SIZEBITS)-1)
#define _IOC_DIRMASK    ((1 << _IOC_DIRBITS)-1)

#define _IOC_NRSHIFT    0
#define _IOC_TYPESHIFT  (_IOC_NRSHIFT+_IOC_NRBITS)
#define _IOC_SIZESHIFT  (_IOC_TYPESHIFT+_IOC_TYPEBITS)
#define _IOC_DIRSHIFT   (_IOC_SIZESHIFT+_IOC_SIZEBITS)

/*
* Direction bits.
*/
#define _IOC_NONE       0U
#define _IOC_WRITE      1U
#define _IOC_READ       2U

#define _IOC(dir,type,nr,size) (((dir)  << _IOC_DIRSHIFT) | ((type) << _IOC_TYPESHIFT) | ((nr)   << _IOC_NRSHIFT) | ((size) << _IOC_SIZESHIFT))

/* used to create numbers */
#define _IO(type,nr)	    _IOC(_IOC_NONE,(type),(nr),0)
#define _IOR(type,nr,size)      _IOC(_IOC_READ,(type),(nr),sizeof(size))
#define _IOW(type,nr,size)      _IOC(_IOC_WRITE,(type),(nr),sizeof(size))
#define _IOWR(type,nr,size)     _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))

/* used to decode ioctl numbers.. */
#define _IOC_DIR(nr)	    (((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK)
#define _IOC_TYPE(nr)	   (((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK)
#define _IOC_NR(nr)	     (((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK)
#define _IOC_SIZE(nr)	   (((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK)

/* ...and for the drivers/sound files... */

#define IOC_IN	  (_IOC_WRITE << _IOC_DIRSHIFT)
#define IOC_OUT	 (_IOC_READ << _IOC_DIRSHIFT)
#define IOC_INOUT       ((_IOC_WRITE|_IOC_READ) << _IOC_DIRSHIFT)
#define IOCSIZE_MASK    (_IOC_SIZEMASK << _IOC_SIZESHIFT)
#define IOCSIZE_SHIFT   (_IOC_SIZESHIFT)
example:
#define LED_ON _IO('k',0)		--> _IO('k',0) --> _IOC(_IOC_NONE,('k'),(0),0)
#define LED_ON_R _IOR('k',1,int) 	--> _IOC(_IOC_READ,'k',1,sizeof(int))
#define LED_ON_W _IOW('k',1,int) 	--> _IOC(_IOC_WRITE,'k',1,sizeof(int))
			
.unlocked_ioctl = myioctl,  --> Add file_operations In structure
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);  --> Application layer ioctl function
	|--> struct file *  : struct file Structure pointer
	|--> unsigned int --> Operation mask (unsigned long request)
	|--> unsigned long --> Variable parameters

6.1.3 predefined commands

Predefined commands fall into three categories:

  • Those that can be issued to any file (general, device, FIFO, or socket)

  • Only those issued for regular files

  • Those that are specific to file system types

The following ioctl commands are predefined for any file, including device specific files:
(1)FIOCLEX
Set the close on exec flag (File IOctl Close on EXec). Set this flag to close the file descriptor when the calling process executes a new program

(2)FIONCLEX
Clear the close no exec flag (File IOctl Not CLose on EXec). This command restores the normal file behavior and the above FIOCLEX. FIOASYNC sets or resets the asynchronous notification for this file (as discussed in the "asynchronous notification" section of this chapter) Note that the kernel of Linux version 2.2.4 does not correctly use this command to modify the O_SYNC flag. Because both actions can be completed through fcntl, no one really uses the fiosync command. It appears here only for integrity

(3)FIOQSIZE
This command returns the size of a file or directory; when used as a device file, however, it returns an ENOTTY error

(4)FIONBIO
"File IOCTL non blocking I / O" (described in the "blocking and non blocking operations" section). This call modifies the _nonblockflag in FILP - > f_flags

The third parameter to this system call is used to indicate whether this flag is set or cleared (we will see the role of this flag in this chapter)
Note that the common method to change this flag is to use fcntl system call and F_SETFL command

6.1.4 using ioctl parameters

Driver ioctl code, how to use the additional unsigned long arg parameter
If it is an integer, it is easy: it can be used directly. If it is a pointer, you must be careful. When referring to user space with a pointer, we must ensure that the user address is valid
Attempting to access an unauthenticated user supplied pointer may result in incorrect behavior, a kernel oops, system crash, or security problems
It is the driver's responsibility to correctly check each user space address it uses and return an error if it is invalid

In Chapter 3, we looked at the copy_from_user and copy_to_user functions, which can be used to safely move data to and from user space. These functions can also be used in ioctl methods, but ioctl calls often contain small data items, which can be operated more effectively by other methods. At first, address verification (no data transfer) is implemented by the function access_ok, which is defined in < ASM / uaccess. H >:

int access_ok(int type, const void *addr, unsigned long size);

The first parameter should be VERIFY_READ or VERIFY_WRITE, depending on whether the action to be performed is to read the user space memory area or write it
The addr parameter holds a user space address, and size is a byte size

If ioctl needs to read an integer from user space, size is sizeof(int). If you need to read and write to a given address, use VERIFY_WRITE because it is VERIRY_READ superset. Unlike most kernel functions, access_ok returns a Boolean value: 1 is success (no access problem) and 0 is failure (no access problem). If it returns false, the driver should return - EFAULT to the caller

  • be careful:
    First, it does not do the complete work of verifying memory access; It only checks that the memory reference is within the memory range for which the process has reasonable permissions. In particular, access_ok, make sure that this address does not point to kernel space memory
    Second, most driver code does not need to actually call access_ok. The memory access function described later is responsible for this for you
scull The source code is used ioclt To check the parameters, stay switch before:
int err = 0, tmp;
int retval = 0;

/*
* extract the type and number bitfields, and don't decode
* wrong cmds: return ENOTTY (inappropriate ioctl) before access_ok()
*/
if (_IOC_TYPE(cmd) != SCULL_IOC_MAGIC){
    return -ENOTTY;
}
if (_IOC_NR(cmd) > SCULL_IOC_MAXNR){
    return -ENOTTY;
}

/*
	* the direction is a bitmask, and VERIFY_WRITE catches R/W
	* transfers. `Type' is user-oriented, while
	* access_ok is kernel-oriented, so the concept of "read" and
	* "write" is reversed
	*/
if (_IOC_DIR(cmd) & _IOC_READ){
    err = !access_ok(VERIFY_WRITE, (void __user *)arg, _IOC_SIZE(cmd));
}
else if (_IOC_DIR(cmd) & _IOC_WRITE){
    err = !access_ok(VERIFY_READ, (void __user *)arg, _IOC_SIZE(cmd));
}
if (err){
    return -EFAULT;
}

When calling access_ After OK, the driver can safely carry out real transmission. Add copy_from_user and copy_to_user_ Function,
Programmers can use a set of functions optimized for the most used data size (1, 2, 4, and 8 bytes). These functions are described in the following list,
They are defined in < ASM / uaccess. H >:

put_user(datum, ptr)
__put_user(datum, ptr)

These macro definitions are written to the user space; They are relatively fast and should be called instead of copy_ to_ Whenever user wants to pass a single value, these macros have been written to allow any type of pointer to be passed to put_user, as long as it is a user space address. The transmitted data size depends on the type of prt parameter, and is determined at compile time by using compiler built-in macros such as sizeof and typeof. The result is that if prt is a char pointer, one byte is transmitted, and for 2, 4, and possibly 8 bytes. Put_ The user check ensures that the process can write to the given memory address. It returns 0 on success and - EFAULT on error

__ put_ The user performs fewer checks (it does not call access_ok), but can still fail if the memory pointed to is not writable to the user__ put_user should only use access in the memory area_ OK. As a general rule, when you implement a read method, call__ put_user saves several cycles, or when you copy several items, therefore, call access_ before the first data transfer. OK once, as shown in ioctl above

get_user(local, ptr)
__get_user(local, ptr)

These macro definitions are used to receive individual data from user space. They are like put_user and__ put_user, but the data is passed in the opposite direction. The obtained value is stored in the local variable local;
The return value indicates whether the operation was successful. Again__ get_user should be used only when access is already in use_ OK verified address

6.1.5 compatibility and restricted operation

The kernel uses exclusive capabilities in license management and outputs two system calls, capget and capset, to allow them to be managed from user space
All capabilities can be found in < Linux / capability. H >. These are the only capabilities available to the system; For driver authors or system administrators,
It is impossible to define a new device driver without modifying the kernel source code. A subset of these capabilities that may be of interest to device driver writers include the following:
(1)CAP_DAC_OVERRIDE
This ability to override access restrictions on files and directories (data access control, or DAC)

(2)CAP_NET_ADMIN
The ability to perform network management tasks, including those that can affect network interfaces

(3)CAP_SYS_MODULE
The ability to load or remove kernel modules

(4)CAP_SYS_RAWIO
The ability to perform "raw" I/O operations. Examples include accessing device ports or communicating directly with USB devices

(5)CAP_SYS_ADMIN
One capture - all capabilities that provide access to many system management operations

(6)CAP_SYS_TTY_CONFIG
Ability to perform tty configuration tasks

6.1.6 implementation of IOCTL command

ioctl of scull The implementation only passes the configuration parameters of the device, And it's as easy as below
switch(cmd)
{
    case SCULL_IOCRESET:
        scull_quantum = SCULL_QUANTUM;
        scull_qset = SCULL_QSET;
        break;
    case SCULL_IOCSQUANTUM: /* Set: arg points to the value */
        if (! capable (CAP_SYS_ADMIN))
            return -EPERM;
        retval = __get_user(scull_quantum, (int __user *)arg);
        break;
    case SCULL_IOCTQUANTUM: /* Tell: arg is the value */
        if (! capable (CAP_SYS_ADMIN))
            return -EPERM;
        scull_quantum = arg;
        break;
    case SCULL_IOCGQUANTUM: /* Get: arg is pointer to result */
        retval = __put_user(scull_quantum, (int __user *)arg);
        break;
    case SCULL_IOCQQUANTUM: /* Query: return it (it's positive) */
        return scull_quantum;
    case SCULL_IOCXQUANTUM: /* eXchange: use arg as pointer */
        if (! capable (CAP_SYS_ADMIN))
            return -EPERM;
        tmp = scull_quantum;
        retval = __get_user(scull_quantum, (int __user *)arg);
        if (retval == 0)
            retval = __put_user(tmp, (int __user *)arg);
        break;
    case SCULL_IOCHQUANTUM: /* sHift: like Tell + Query */
        if (! capable (CAP_SYS_ADMIN))
            return -EPERM;
        tmp = scull_quantum;
        scull_quantum = arg;
        return tmp;
    default: /* redundant, as cmd was checked against MAXNR */
        return -ENOTTY;
}
return retval;

From the caller's point of view (i.e. from user space), the six methods of passing and receiving parameters appear as follows:

int quantum;
ioctl(fd,SCULL_IOCSQUANTUM, &quantum); /* Set by pointer */
ioctl(fd,SCULL_IOCTQUANTUM, quantum); /* Set by value */
ioctl(fd,SCULL_IOCGQUANTUM, &quantum); /* Get by pointer */
quantum = ioctl(fd,SCULL_IOCQQUANTUM); /* Get by return value */
ioctl(fd,SCULL_IOCXQUANTUM, &quantum); /* Exchange by pointer */

quantum = ioctl(fd,SCULL_IOCHQUANTUM, quantum); /* Exchange by value */

Tags: Linux Operation & Maintenance server

Posted on Sun, 05 Dec 2021 23:38:30 -0500 by Sikk Industries