Typical usage for controlling a device under Linux consists of using a small set of function calls, usually called system calls, because they are implemented by the operating system's kernel. The interface is virtually identical to the one used for regular disk files. This standardized method of accessing devices means programmers don't need to learn new functions for each type of device. Programs can be written that will work with many different types of devices as well as disk files.
The open system call establishes access to the device, returning a file descriptor to be used for subsequent calls. The read and write functions receive data from and send data to a device, respectively. The ioctl routine is a catch-all function to perform other operations that do not fit the read/write model. For instance, to set mixer gains, the mixer driver offers an ioctl command that would be meaningless in any other devices.
Finally, close is used to notify the operating system that the device is no longer in use. The operating system normally closes all open devices when a program exits, but it is good practice to explicitly close them in your application. Most devices support a subset of these operations. For example, some devices may be read only, and not support the write function.
Some C programmers may not be familiar with the read and write functions. File and terminal access typically uses the buffered input/output routines such as printf and scanf. These are usually more efficient for files because the reads and writes are buffered in memory and performed later in larger blocks, reducing the overhead associated with calling the kernel read and write routines repeatedly. For low-level access to multimedia devices, you normally do not want this--you generally want data to be serviced immediately and have explicit control over buffer sizes. I will now look at each of these system calls in more detail.
This system call follows the format:
int open(const char *pathname, int flags, int mode);
This function is used to gain access to a device so you can subsequently operate on it with other system calls. The device or file can be an existing one that is to be opened for reading, writing, or both. It can also be used to create a new file.
The pathname parameter is the name of the file to be operated on. It can be a regular file, or a device file such as /dev/dsp. The flags parameter indicates the mode to be used for opening the file, and takes one of the following values:
open for read only
open for write only
open for both read and write
In addition, some flags can be "bitwise OR" with the ones above to control other aspects of opening the file. A number of flags are defined, most of which are device-specific and not important to our discussion here.
The third mode parameter is optional--it specifies file permissions to be used when creating a new file and is only used when the O_CREAT option is given.
The open call, if successful, returns an integer file descriptor (a small positive number) to be used in subsequent system calls to reference the file. If the open fails for some reason, the call returns -1 and sets the variable errno to a value indicating the reason for failure.
There are some other more obscure options not relevant to our purposes; see the open(2) manpage for details.
The format of this function is:
int read(int fd, char *buf, size_t count);
This call returns data from a file or device. The first parameter is a file descriptor, obtained from a previous call to open. The buf parameter points to a buffer in which to hold the data returned--a sequence of bytes. The char * definition for the buffer is a convenience to cover all kinds of data. The argument is often cast to another data type, such as a data structure, that represents the particular kind of data you're dealing with. The count parameter indicates the maximum number of bytes to be read. If successful, the function returns the actual number of bytes read, which is sometimes less than count. On error, the value -1 is returned and the global variable errno is set to a value indicating the error cause.
Calling read can cause a process to block until the data is available.
Writing data uses the write system call, which takes the form:
size_t write(int fd, const char *buf, size_t count);
This function is analogous to read, but sends data to a file or device. Parameter fd is the open file descriptor, buf points to the data to be written, and count indicates the number of bytes to be written. The function returns the number of bytes actually written, or -1 if an error occurred. Like the read call, the process may be blocked by the kernel until the data has been successfully written.
The ioctl system call, a catch-all function, takes the form:
int ioctl(int fd, int request, ...);
This function is used for performing miscellaneous operations on a file or device that does not fit into the read or write calls. Each request may set some behavior of the device, return information, or both. It is device-specific.
The first parameter is a file descriptor, obtained when the device was opened. The second is an integer value indicating the type of ioctl request being made. There is usually a third parameter, which is dependent on the specific ioctl request being made.
Later in the chapter I will show some examples of using ioctl on multimedia sound devices.
The last of the basic functions follows this format:
int close(int fd);
The close system call notifies the kernel that access to a file or device is no longer required, allowing any related resources to be freed up.
As there is a limit on the number of files that any one process can have open at one time, it is good practice to close files or devices when you are finished with them.
The simple program in Example 14-1 illustrates most of the concepts discussed so far.Example of Linux System Calls
/*
* syscalls.c
* Program to illustrate common system calls. Doesn't actually
* perform any useful function, but will later be expanded into
* a program which does.
*/
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <linux/soundcard.h>
int main()
{
int fd; /* device file descriptor */
int arg; /* argument for ioctl call */
unsigned char buf[1000]; /* buffer to hold data */
int status; /* return status of system calls */
/* open device */
status = fd = open("/dev/dsp", O_RDWR);
if (status == -1) {
perror("error opening /dev/dsp");
exit(1);
}
/* set a parameter using ioctl call */
arg = 8000; /* sampling rate */
status = ioctl(fd, SOUND_PCM_WRITE_RATE, &arg);
if (status == -1) {
perror("error from SOUND_PCM_WRITE_RATE ioctl");
exit(1);
}
/* read some data */
status = read(fd, buf, sizeof(buf));
if (status == -1) {
perror("error reading from /dev/dsp");
exit(1);
}
/* write some data */
status = write(fd, buf, sizeof(buf));
if (status == -1) {
perror("error writing to /dev/dsp");
exit(1);
}
/* close the device */
status = close(fd);
if (status == -1) {
perror("error closing /dev/dsp");
exit(1);
}
/* and exit */
return(0);
}
First I include the header files that define the library routines used in the program. An easy way to identify these is to read the relevant manpages for the functions. I then start the function main, the only one in this small program, and define the variables needed to hold the file descriptor, the argument to ioctl, the data buffer, and the status returned by the system calls used.
I open the device file /dev/dsp, indicating to open for both read and write. The third parameter is not needed as I am not creating a new file. After calling open, I check the return value, which displays an error message and exits if the call was not successful. I then use the ioctl call to set a parameter of the device. The meaning and type of the argument is specific to this ioctl function, but can be ignored for now. I'll cover it later.
Next I call read to obtain some data bytes from the device. I again check the return status. Then the same data is written back in a similar manner using the write system call.
The last step is to close the device, and again I check the status of the call to close, although some programmers might consider this level of checking a bit paranoid.
If you are new to C programming under Linux, I recommend that you enter the example program on your system and run it. Don't worry yet about what it does, just concentrate on successfully compiling it and verifying that it runs without errors. Try changing the program so that it attempts to operate on a nonexistent device and check that an error message is produced. Can you think of ways to produce any other error messages from the sample program?
Sound Programming Basics
Programming Sound Devices