“This is the 18th day of my participation in the Gwen Challenge in November. See details: The Last Gwen Challenge in 2021.”
1.1 the read/write
The read function reads data from an open device or file.
#include <unistd.h> ssize_t read(int fd, void *buf, size_t count); // Return value: the number of bytes read on success, -1 on error and errno, or 0 this time if the end of the file was reached before the call to readCopy the code
The count parameter is the number of bytes requested to be read. The read data is stored in buffer BUF, and the current read/write position of the file is moved back.
Note that this read/write position may be different from the read/write position recorded in the kernel when using the C standard I/O library, which is the position in the user-space I/O buffer. For example, if fgeTC is used to read a byte, it is possible for fGETC to read 1024 bytes from the kernel into the I/O buffer and then return the first byte. In this case, the FILE is recorded in the kernel as read/write position 1024 and in the FILE structure as read/write position 1.
Note that the return value type is SSIze_t, indicating a signed size_t, which can either return a positive number of bytes, 0(the table reaches the end of the file) or a negative value -1(indicating an error).
When the read function returns, the return value specifies how many of the first bytes in buf were just read. In some cases, the number of bytes actually read (the return value) is less than the number of bytes requested, such as count:
-
When reading a regular file, the end of the file is reached before count bytes are read. For example, if there are 30 bytes left until the end of the file and a request for 100 bytes, read returns 30, and the next time read returns 0.
-
Read from a terminal device, usually in units of behavior, and returned at a newline character.
-
Read from the network. Depending on the transport layer protocol and the kernel caching mechanism, the return value may be less than the requested bytes, as explained later in the socket programming section.
The write function writes data to an open device or file.
#include <unistd.h> ssize_t write(int fd, const void *buf, size_t count); Return value: Returns the number of bytes written on success, -1 on error and sets errnoCopy the code
When writing to a regular file, the return value of write is usually equal to the number of bytes requested, but not necessarily to a terminal device or network.
1.2 Blocking and non-blocking
Reading regular files does not block, and no matter how many bytes are read, read is bound to return within a finite amount of time. Read from the terminal or network is not necessarily, if there is no newline data input from a terminal, terminal equipment will be blocked calls read read, if not received packets on the network, calls read read will be blocked from the network, also not sure as to how long it will be blocked, if there has been no data to reach there has been blocked. Similarly, writing to regular files does not block, while writing to terminal devices or the network does not.
Now let’s clarify the concept of a Block. When a process calls a blocking system function, the process is put into Sleep state, at which point the kernel schedules other processes to run until the event for which the process is waiting (such as receiving a packet on the network, or the Sleep time specified by the Sleep call) occurs. The opposite of a sleeping state is a Running state. In the Linux kernel, a Running process is classified into two conditions:
- Being scheduled for execution. The CPU is in the context of the process, the program counter (EIP) stores the process’s instruction address, the general register stores the process’s intermediate results of the operation process, is executing the process’s instruction, is reading and writing the process’s address space.
- Ready state. The process doesn’t have to wait for anything to happen and can execute at any time, but the CPU is still executing another process, so it’s in a ready queue waiting to be scheduled by the kernel. There may be multiple ready steps in the system at the same time, so who should be scheduled to execute them? The scheduling algorithm of the kernel is based on the priority and time slice, and will dynamically adjust its priority and time slice according to the running status of each process, so that each process can get a fair chance to execute, and at the same time, the user experience should be taken into account, and the process that interacts with the user should not respond too slowly.
The following little program reads data from the terminal and writes it back.
1.2.1 Blocking the Read Terminal
#include <unistd.h> #include <stdlib.h>
int main(void) {
char buf[10];
int n;
n = read(STDIN_FILENO, buf, 10);
if (n < 0) {
perror("read STDIN_FILENO");
exit(1);
}
write(STDOUT_FILENO, buf, n);
return 0;
}
Copy the code
The result is as follows:
$./a.out hello(Enter) Hello $./a.out hello world(enter) Hello worl$d bash: d: Command not foundCopy the code
The result of the first execution of A.out is normal, while the process of the second execution is a little special, now analyze:
Shell creates a.out, a.out starts executing, and Shell sleeps waiting for A.out to exit.
A. out calls read and waits until the terminal returns a newline. Read only takes 10 characters, and the rest of the characters are still stored in the terminal device input buffer in the kernel.
The a.out process prints and exits. At this time, the shell process resumes and continues to read the command entered by the user from the terminal. The shell then reads the remaining character D and newline in the input buffer of the terminal device and considers it as a command interpretation line.
If the O_NONBLOCK flag is specified when opening a device, read/write does not block. For example, if the device has no data to read for the time being, -1 is returned, and errno is EWOULDBLOCK(or EAGAIN, the two macros have the same value), indicating that would block would not have occurred and an error would have been returned. The caller should try to read it again. This behavior is called polling, where the caller just queries, rather than blocking and waiting, so that multiple devices can be monitored at the same time:
While (1) {non-blocking read(device 1); If (device 1 has data arriving) processes data; Non-blocking read(device 2); If (device 2 has data arriving) processes data; . }Copy the code
If read(device 1) is blocked, then read calls to device 1 will be blocked as long as data arrives from device 1 and cannot be processed even if data arrives from device 2. Using non-blocking I/O can avoid device 2 not being processed in a timely manner.
One disadvantage of non-blocking I/O is that if no data ever arrives on any of the devices, the caller will need to repeatedly query for useless work. If the blocking is there, the operating system can schedule other processes to execute it, and no useless work will be done. When using non-blocking I/O, it is common not to query continuously in a while Loop (this is called a Tight Loop), but to delay the query for a few moments at a time to avoid doing too much useless work, and to schedule other processes to execute while the delay takes place.
While (1) {non-blocking read(device 1); If (device 1 has data arriving) processes data; Non-blocking read(device 2); If (device 2 has data arriving) processes data; . sleep(n); }Copy the code
The problem with this is that device 1 May not be able to process data in time when it arrives, with a delay of up to N seconds. In addition, repeated queries still do a lot of useless work. The select(2) function, which you’ll learn about later, solves this problem by blocking multiple devices at the same time and by setting a timeout for the blocking wait.
Here is an example of non-blocking I/O. So far the only devices we’ve seen that can cause blocking are terminals, so let’s do this with terminals. Files that are automatically opened at the 0, 1, 2 file descriptors when the program starts execution are endpoints, but do not have the O_NONBLOCK flag. So just as in example 28.2, “Blocking the read terminal,” the read standard input is blocked. We can re-open the device file /dev/tty(for the current terminal) and specify the O_NONBLOCK flag when we open it.
1.2.2 Non-blocking read terminals
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#define MSG_TRY "try again\n"
int main(void) {
char buf[10];
int fd, n;
fd = open("/dev/tty", O_RDONLY|O_NONBLOCK);
if(fd<0) {
perror("open /dev/tty");
exit(1);
}
tryagain:
n = read(fd, buf, 10);
if (n < 0) {
if (errno == EAGAIN) {
sleep(1);
write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
goto tryagain;
}
perror("read /dev/tty");
exit(1);
}
write(STDOUT_FILENO, buf, n); close(fd);
return 0;
}
Copy the code
The following is an example of a wait timeout implemented with non-blocking I/O. It not only ensures the logic of timeout exit but also ensures the small processing delay when data arrives.
1.2.3 Non-blocking Read Terminals and Wait Timeout
#include <unistd.h> #include <fcntl.h> #include <errno.h> #include <string.h> #include <stdlib.h> #define MSG_TRY "try again\n" #define MSG_TIMEOUT "timeout\n" int main(void) { char buf[10]; int fd, n, i; fd = open("/dev/tty", O_RDONLY|O_NONBLOCK); if(fd<0) { perror("open /dev/tty"); exit(1); } for(i=0; i<5; i++) { n = read(fd, buf, 10); if(n>=0) break; if(errno! =EAGAIN) { perror("read /dev/tty"); exit(1); } sleep(1); write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY)); } if(i==5) write(STDOUT_FILENO, MSG_TIMEOUT, strlen(MSG_TIMEOUT)); else write(STDOUT_FILENO, buf, n); close(fd); return 0; }Copy the code
1.3 lseek
Each open file records the current read/write position. When the file is opened, the read/write position is 0, indicating the beginning of the file. Usually, the read/write position will be moved by the number of bytes read/write. With one exception, if opened in O_APPEND, each write operation appends data to the end of the file and then moves the read/write position to the new end of the file. Similar to the standard I/O library’s fseek function, lseek moves the current read/write position (or offset).
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
Copy the code
The arguments offset and whence have exactly the same meaning as the fseek function. It’s just that the first argument has been replaced with a file descriptor. As with Fseek, offsets are allowed past the end of the file, in which case the next write to the file will lengthen the file, and any empty space in the middle will read as zeros.
If lseek executes successfully, the new offset is returned, so the current offset of an open file can be determined using the following methods:
off_t currpos;
currpos = lseek(fd, 0, SEEK_CUR);
Copy the code
This method can also be used to determine whether a file or device can set offsets. Regular files can set offsets, but devices generally cannot. If the device does not support LSEEK, lSEEK returns -1 and sets errno to ESPIPE. Note that fseek and Lseek have slightly different return values. Fseek returns 0 on success and -1 on failure, ftell is called to return the current offset, and lseek returns -1 on success and -1 on failure.
1.4 an FCNTL
Previously we introduced non-blocking I/O using the read terminal device as an example. Why don’t we just do non-blocking read on STDIN_FILENO instead of reopening /dev/tty? Because STDIN_FILENO is automatically turned on when the program starts, we need to specify the O_NONBLOCK flag when we call open. Here is another way to change the properties of an open File using the FCNTL function. You can reset read, write, append, non-blocking flags (called File Status flags) without having to reopen the File.
#include <unistd.h> #include <fcntl.h>
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);
Copy the code
This function, like open, is implemented with mutable arguments, the types and number of which depend on the CMD arguments above. The following example uses the F_GETFL and F_SETFL FCNTL commands to change the STDIN_FILENO property and add the O_NONBLOCK option to achieve the same function as example 28.3 “Non-blocking read terminal”.
1.4.1 Changing the File Status Flag using FCNTL
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#define MSG_TRY "try again\n"
int main(void) {
char buf[10];
int n;
int flags;
flags = fcntl(STDIN_FILENO, F_GETFL); flags |= O_NONBLOCK;
if (fcntl(STDIN_FILENO, F_SETFL, flags) == -1) {
perror("fcntl");
exit(1);
}
tryagain:
n = read(STDIN_FILENO, buf, 10);
if (n < 0) {
if (errno == EAGAIN) {
sleep(1);
write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
goto tryagain;
}
perror("read stdin");
exit(1);
}
write(STDOUT_FILENO, buf, n);
return 0;
}
Copy the code
1.5 the ioctl
Ioctl is used to send control and configuration commands to the device. Some commands also need to read and write data that cannot be read or written using read/write. These commands are called out-of-band data. In other words, read/write data is in-band data, which is the body of the I/O operation, while ioctl commands transmit control information, which is secondary data. For example, data sent and received on the serial cable is read/write. The baud rate, check bit, and stop bit of the serial port are set through the IOCTL. The result of A/D conversion is read through the READ, and the accuracy and operating frequency of A/D conversion are set through the IOCTL.
#include <sys/ioctl.h> int ioctl(int d, int request, ...) ;Copy the code
D is the file descriptor of a device. Request is an IOCtl command, and the variable argument depends on the request, usually a pointer to a variable or structure. Returns -1 on error, or something else on success, depending on request.
The following program uses the TIOCGWINSZ command to get the window size of the terminal device.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
int main(void) {
struct winsize size;
if (isatty(STDOUT_FILENO) == 0)
exit(1);
if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &size)<0) {
perror("ioctl TIOCGWINSZ error");
exit(1);
}
printf("%d rows, %d columns\n", size.ws_row, size.ws_col);
return 0;
}
Copy the code
Change the size of the terminal window several times in the graphical interface and run the program to observe the results.
1.6 summarize
This article introduces read/write system calls, concepts related to blocking and non-blocking, configuration methods, and wait timeout methods. System calls related to lSEEK, FCNTL and IOCTL file operations are also introduced.