More high quality original articles are in it

Host byte order and network byte order:

On 32-bit machines, the accumulator can load up to four bytes at a time, and the memory order of these four bytes will affect the value of the integer loaded by the accumulator

Big endian (network endian) : The highest byte of an integer is stored at the lowest address in memory

Small-endian (most modern PCS) : The most significant bytes of an integer are stored at the highest address in memory

Even when programs written in different languages on the same machine communicate, byte order is a concern

Linux bytecode conversion function:

 #include<netinet/in.h>
 unsigned long int htol (unsigned long int hostlong); // Host byte order is converted to network byte order
 unsigned short int htons (unsigned short int hostshort);// Host byte order is converted to network byte order
 unsigned long int ntohl (unsigned long int netlong);// Network byte order is converted to host byte order
 unsigned short int ntohs (unsigned short int netshort);// Network byte order is converted to host byte order
Copy the code

The socket address

 #include<bits/sockets.h>
 struct sockaddr{
 	sa_family_t sa_family;	// Address family type variables correspond to protocol family
 	char sa_data[14];	// Store the socket address
 }
Copy the code
Protocol family The family of address describe Address value meaning and length
PF_UNIX AF_UNIX UNIX local area protocol family The path name of the file, which can be 108 bytes long
PF_INET AF_INET TCP/IPv4 protocol family 16-bit port number and 32-bit IPv4 address, 6 bytes
PF_INET6 AF_INET6 TCP/IPv6 protocol family 16bit port number, 32bit stream ID, 128bit IPv6 address, 32bit range ID, 26 bytes in total

To accommodate most protocol family address values, Linux redefines the socket address structure

#include<bits/socket.h>
struct sockaddr_storage{
    sa_family_t sa_family;
    unsigned long int __ss_align;	// it is memory aligned
    char __ss_padding[128-sizeof(__ss_align)];
}
Copy the code

Linux has two dedicated socket address structures for the TCP/IP protocol family, sockaddr_in and sockaddr_in6, which are used for IPv4 and IPv6 respectively

 // For IPv4:
 struct sockaddr_in{
 	sa_family sin_family;	// Address family: AF_INET
 	u_int16_t sin_port;		// The port number, in network byte order
 	struct in_addr sin_addr;//IPv4 address structure
 }
 // The IPv4 structure
 struct in_addr
 {
 	u_int32_t s_addr;	// Use network byte order
 }
 / / for IPv6
 struct sockaddr_in6{
 	sa_family_t sin6_family;//AF_INET6
 	u_int16_t sin6_port;	// The port number, in network byte order
 	u_int32_t sin6_flowinfo;// Stream information, should be set to 0
 	struct in6_addr sin6_addr;//IPv6 address structure
 	u_int32_t sin6_scope_id;//scope ID is in the experimental phase
 }
 // The IPv6 structure
 struct in6_addr
 {
 	unsigned char sa_addr[16];	// Use network byte order
 }
Copy the code

The socket address type must be cast to the common socket address type Socketaddr

IPv4 address translation between an IPv4 address expressed as a string in dotted decimal notation and an integer in network byte order

 #incldue<arpa/inet.h>
 in_addr_t inet_addr(const char* strptr);			// Dotted decimal notation --> Network byte order integer. On failure, INADDR_NONE is returned
 int inet_aton (const char* cp,struct in_addr* inp);// The result is stored in the address structure pointed to by the inp parameter, and returns 1 on success and 0 on failure
 char* inet_ntoa (struct in_addr in);	Inet_ntoa is not reentrant. Inet_ntoa is not reentrant. Inet_ntoa is not reentrant
Copy the code
// Same function as above, can be used for IPv6
#include<arpa/inet.h>
int inet_pton(int af,const char* src,void* dst);// Store the result in DST memory, where af represents the protocol family ---- returns 1 on success, 0 on failure and set error
const char* inet_ntop(int af,const void* src,char* dst,socklen_t cnt);/ / in the same way


// The following two macros help us specify the size of the CNT
#include<netinet/in.h>
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
Copy the code

Create a socket

Everything on Linux is a file

 #include<sys/types.h>
 #include<sys/socket.h>
 int socket (int domain,int type ,int protocol);// The domain parameter represents the underlying protocol family (IPv4 uses PF_INET). The Type parameter specifies the service types into SOCK_STREAM (TCP) and SOCK_DGRAM (UDP). The protocol parameter is the first two Select a specific protocol (in almost all cases it is set to 0, indicating that the default protocol is used)
Copy the code

The socket system call returns a socket file descriptor on success, -1 and errno on failure

Named after the socket

The socket is created and the address family is specified, but the socket address in the address family is not specified

Binding a socket to a socket address is called naming the socket

Clients usually do not need to name sockets, but use anonymous socket addresses automatically assigned by the operating system

 #include<sys/types.h>
 #include<sys/socket.h>
 int bind (int sockfd,const struct sockaddr* my_addr,socklen_t addrlen)// Bind assigns the socket address referred to by my_addr to the unnamed sockfd file descriptor. The addrlen parameter indicates the length of the socket address. Bind returns 0 on success, -1 on failure, and sets errno** Two common errnos are EACCES and EADDRINUSE** **EACCCES: bound addresses are protected addresses accessible only to superusers. ** **EADDRINUSE: ** ## The address to which the socket is bound is in use (e.g., to a socket in TIME_WAIT state). C++ #include<sys/socket.h> int listen (int sockfd,int backlog);The backlog parameter indicates the maximum length of the queue to be listened on. If the backlog value exceeds the total length of the queue, the server will not accept new client connections and the client will receive ECONNREFUSED
Copy the code

Prior to Kernel version 2.2: The backlog parameter refers to the maximum number of sockets in the semi-connected state (SYN_RCVD) and fully connected state (ESTABLISHED)

After kernel version 2.2: This represents only the upper limit for fully connected sockets, the upper limit for semi-connected sockets, as defined in the tcp_max_syn_backlog kernel parameter.

The backlog argument typically has a value of 5 and returns 0 on success or -1 on failure with errno

Accept connections

 #include<sys/types.h>
 #include<sys/socket.h>
 int accept(int sockfd , struct sockaddr *addr,socklen_t *addrlen);The sockfd argument is the listening socket that performed the LISTEN system call. The addr parameter is used to obtain the remote socket address for the accepted connection. The length of the socket address is indicated by the addrlen parameter. Accept returns a new connection socket that uniquely identifies the accepted connection. The server can read and write the socket to communicate with the client corresponding to the accepted connection. On failure -1 is returned and errno is set.
Copy the code

A connection

 #include<sys/types.h>
 #include<sys/socket.h>
 int connect(int sockfd, const struct sockaddr *serv_adr,socklen_t addrlen);The system call returns a socket. The serv_addr parameter is the socket address that the server listens for. The addrlen parameter is specified
Copy the code

Once the connection is successfully established, the sockFD uniquely identifies the connection, and the client can communicate with the server by reading and writing the SockFD. Failure returns -1 and sets errno

ECONNREFUSED: the destination port does not exist and the connection is refused. ETIMEDOUT: The connection times out

Close the connection

 #include<unistd.h>
 int close(int fd);	The fd argument is the socket to be closed, but instead of closing the connection immediately, the fd reference count is subtracted by one and the connection is closed when it reaches zero
Copy the code

In a multi-process program, a system call increments the reference count of the socket opened in the parent process by one by default, so we must call close on the socket in both the parent and child processes to close the connection

If you want to terminate the connection immediately anyway, you can use the shutdown system call

 #include<sys/socket.h>
 int shutdown (int sockfd,int howto);// The sockfd parameter is the socket to be closed, and the hoWTO parameter determines the shutdown behavior
Copy the code
An optional value meaning
SHUT_RD Close this half of the read on sockfd. The application no longer performs reads against the socket file descriptor, and all data in the socket receive buffer is discarded
SHUT_WR Close the half written on sockfd. The data in the sockFD send buffer is sent out before the connection is actually closed, and the application can no longer write to the SockFD file descriptor. In this case, the connection is in a semi-connected state
SHUT_RDWR Close both read and write on sockFD

Shutdown shuts down read and write on a SOckFD separately, or both. Close can only close the read and write on the SOckFD at the same time

0 is returned on success, -1 is returned on failure, and errno is set

Read and write data

TCP Data read and write

 #include<sys/types.h>
 #include<sys/socket.h>
 ssize_t recv (int sockfd , void *buf ,size_t len ,int flags);	Recv reads data from the sockfd. The buf and len parameters specify the location and size of the read buffer, respectively. The flags parameter is set to 0. Success returns the length of the data actually read, which may be less than our desired length len. So we might have to call it multiple times. Returns 0, which means the peer has closed the connection, -1 in case of an error, and sets errno.
 ssize_t send (int sockfd , const void *buf ,size_t len,int flags);//send writes data to the sockfd, buf and len are still the location and size of the cache. If send succeeds, the actual write length is returned. If send fails, -1 is returned and errno is set.
Copy the code

The FLAGS parameter provides additional control

UDP data Read and write

 #include<sys/types.h>
 #include<sys/socket.h>
 ssize_t recvfrom (int sockfd ,void* buf , size_t len, int flags , struct sockaddr* src_addr ,socklen_t* addrlen);// RecvFROM reads data from sockfd. The buf and len parameters specify the location and size of the read buffer, respectively. The addrlen parameter specifies the length of the address
 ssize_t sendto (int sockfd , const void* buf ,size_t len,int flags ,const struct sockaddr* dest_addr, socklen_t addrlen );// sendto writes data to sockfd. The buf and len arguments specify the location and size of the write buffer, respectively. The dest_addr parameter specifies the socket address of the receiving end. The addrlen parameter specifies the length of the address
 //flag has the same meaning as the preceding
Copy the code

These two parameters can also be used to read or write data on connection-oriented sockets, by setting the last two parameters to NULL to ignore the sender/receiver socket address.

General data read and write functions

#include<sys/socket.h>
ssize_t recvmsg (int sockfd, struct msghdr* msg ,int flags);
ssize_t sendmsg (int sockfd ,struct msghdr* msg,int flags);
/ / MSGHDR structure
struct msghdr
{
    void* msg_name;	// The socket address is not available for TCP connections because the address is already known
    socklen_t msg_namelen;// The length of the socket address
    struct iovec* msg_lov;// Scatter memory blocks // encapsulate locations and sizes // array
    int msg_iovlen;// The number of memory blocks scattered
    void* msg_control;// Point to the starting position of the secondary data
    socllen_t msg_controllen;// Secondary data size
    int msg_flags;// Assign the flags argument in the function and update it during the call
}
struct iovec{
    void *iov_base;	// Start memory address
    size_t iov_len;  // The length of the memory block
}
Copy the code

For recvMSG, the data will be read and stored in the msG_iovlen block’s scattered memory. The location and length of the memory are specified by the array pointed to by MSG_iov. This is called scattered read. For sendMsg, data in the MSG_iovlen block scattered in memory will be sent together, which is called centralized write

Out-of-band tag

 #include<sys/socket.h>
 int sockatmark (int sockfd);// Check whether sockfd is in the out-of-band flag, that is, whether the next data to be read is out of band. If it is, it returns 1, in which case a recV call with the MSG_OOB flag can be used to receive out-of-band data, or if it is not, it returns 0
Copy the code

Address information function

 #include<iosstream>
 int getsockname (int sockfd,struct sockaddr* address, socklen_t* address_len);// Obtain the local SOckfd address and store it in the memory specified by address. The length is stored in the variable specified by address_len. If the actual length is greater than the memory specified by address, the socket address will be truncated. Returns 0 on success, -1 on failure, and sets errno
 int getpeername (int sockfd, struct sockaddr* address , socklen_t* address_len);// Obtain the remote socket address corresponding to the SOckfd
Copy the code

The socket option

 #include<sys/socket.h>
 int getsockopt (int sockfd,int level,int option_name , void* option_value);The sockfd parameter specifies the target socket to be operated on. The level parameter specifies which protocol option to operate on, the option_name parameter specifies the name of the option, and the option_value and option_len parameters are the value and length of the option to operate on, respectively
 int setsockopt (int sockfd , int level ,int option_name ,const void* option_value,socklen_t option_len);
Copy the code

Both functions return 0 on success, -1 on failure and set errno

Network information API

 // Obtain complete information about the host based on the host name
 #include<neidb.h>
 struct hostent* gethostbyname (const char* name);
 struct hostent* gethostbyaddr (const void* addr ,size_t len, int type);
 
 #include<netdb.h>
 struct hostent{
     char* h_name;	/ / host name
     char** h_aliases;// A list of host aliases, possibly multiple
     int h_addrtype;	// Address type (address family)
     int h_length;	// Address length
     char** h_addr_list;// List of host IP addresses by network byte sequence
 }
Copy the code
#include<netdb.h> struct servent* getServbyName (const char* name,const char* proto); struct servent* getservbyport (int port ,const char* proto); #include<netdb.h> struct servent{ char* s_name; // service name char** s_aliases; // List of aliases for services, possibly multiple int s_port; Char * s_proto; // Service type, usually TCP or UDP}Copy the code
 ---- GeiHostByName and getServByName are used internally
 #include<netdb.h>
 int getaddrinfo (const char* hostname ,const char* service ,const struct addrinfo* hints ,struct addrinfo** result);
 
 struct addrinfo
 {
     int ai_flags;
     int ai_family;	/ / address family
     int ai_socktype;// Service type, SOCK_STREAM or SOCK_DGRAM
     int ai_protocol;
     socklen_t ai_addrlen;// The length of the socket address ai_addr
     char* ai_canonname;// Alias of the host
     struct sockaddr* ai_addr;// points to the socket address
     struct addrinfo* ai_next;// Points to the object of the next sockInfo structure
 }
Copy the code

This function will allocate heap memory implicitly, so we need to pair the following functions

 // Free up memory
 #include<netdb.h>
 void freeaddrinfo (struct addrinfo* res);
Copy the code
 // Store the returned host name in the cache pointed by the HSOt parameter and the service name in the cache pointed by the serv parameter. The hostlen and servlen parameters specify the length of the two caches respectively
 #include<netdb.h>
 int getnameinfo (const struct sockaddr* sockaddr,socklen_t addrlen,char* host,socklen_t hostlen,char* serv,socklen_t servlen,int flags);
Copy the code

Advanced I/O functions

 The pipe function can be used to create a pipe for interprocess communication
 #include<unistd.h>
 int pipe( int fd[2]);// The argument is an array pointer to two ints. On success, the function returns 0 and fills the array to which the argument points with the value of the open file descriptor. On failure, the function returns -1 and sets errno
 //fd[0] can only be used to read data from a pipe, and fd[1] can only be used to write data into a pipe, not vice versa
Copy the code
 // make it easy to create two-way pipes
 #include<sys/types>
 #include<sys/socket.h>
 int socketpair (int domain ,int type ,int protocol ,int fd[2]);
 //dpmain can only use AF_UNIX, which can only be used locally. The last parameter is the same as in the PIPE system call, except that socketpair creates both file descriptors that are readable and writable, returning 0 on success and -1 on failure with errno
Copy the code
 // Redirect standard input to a file or network
 #include<unistd.h>
 int dup (int file_descriptor);
 int dup2 (int file_descriptor_one, int file_descriptor_two);
Copy the code
 // Read and write separately
 #include<sys/uio.h>
 ssize_t readv (int fd, const struct iovec* vector ,int count);
 ssize_t writev (int fd , const struct iovec* vector, int count);
 // Vector stores ioVEC arrays. Count is the length of the vector
Copy the code
 // Passing data between the two file descriptors (operating entirely within the kernel) avoids copying data between the kernel and user buffers, which is very efficient and is called -------- zero copy
 #include<sys/sendfile.h>
 ssize_t sendfile (int out_fd,int in_fd, off_t* offest ,size_t count);
 The in_fd argument is the file descriptor of the content to be read, the out_fd argument is the file descriptor of the content to be written, the offest argument specifies where the file stream should be read from, if empty, the default start of the file stream is used, and the count argument specifies the number of bytes transferred between the file descriptors
Copy the code
 // Apply for a memory space
 #include<sys/mman.h>
 void* mmap (void *start ,size_t length,int prot ,int flags ,int fd,off_t offest);
 int munmap (void *start,size_t length);
 //start allows the user to use a specific address as the start address, length specifies the length of the memory segment, and port specifies the access permission for the memory segment
 //PROT_READ Memory segment readable
 //PROT_WRITE Memory segment writable
 //PROT_EXEC memory segment can be executed
 //PROT_NONE Memory segment cannot be accessed
Copy the code

 // Used to move data between two file descriptors ---- zero copy
 #include<fcntl.h>
 ssize_t splice (int fd_in ,loff_t* off_in ,int fd_out , loff_t* off_out,size_t len, unsigned int flags);
 //fd_int If it is a pipe file descriptor, off_in is set to NULL. If not, the off_in parameter indicates where the data was read from in the input data stream, and if not NULL, the specific offset position. Similarly, the fd_out and off_out parameter specifies the length of the data to move
Copy the code

 // Copy data between two pipe file descriptors, i.e., zero copy operation
 #include<fcntl.h>
 ssize_t tee (int fd_in ,int fd_out ,size_t len ,unsigned int flags);
 // The parameters are the same as splice
Copy the code
 // Provides various control operations on file descriptors
 #include<fcntl.h>
 int fcntl (int fd,intCMD,...);
 The fd argument is the file descriptor to be operated on, the CMD argument specifies what to do, and depending on the type, a third optional argument, arg, may be required
Copy the code

Linux server program specification

Server program specification:

Linux server programs usually run —— daemon in the background. Linux server programs usually have a logging system that can at least output logs to files. Some advanced servers can also output logs to a dedicated UDP server. Linux server programs are usually run as a dedicated non-root user. Linux server programs are usually configurable. The server can usually handle many command-line options. Most server programs have configuration files stored in the /etc directory. Linux server programs usually generate a PID file at startup and store the PID file in the /var/run directory to record the background process. Linux server programs usually need to consider system resources and limitations. To predict how much load they can take

The log

 #include<syslog.h>
 void syslog (int priority ,const char* message , ...)
 The priority parameter is the so-called bit-or of the facility value and log level. The default value is LOG_USER
 
 // Log level
 #include<syslog.h>
 #define LOG_EMERG    0// The system is unavailable
 #define LOG_ALERT	 1// Alarm, need to understand immediate action
 #define LOG_CRIT	 2// Very serious situation
 #define LOG_ERR		 3/ / error
 #define LOG_WARNING	 4/ / warning
 #define LOG_NOTICE	 5/ / notice
 #define LOG_INFO	 6/ / information
 #define LOG_DEBUG    7/ / debugging
     
 // Change the default syslog output mode to further structure the log content
 #include<syslog.h>
 void openlog (const char* ident ,int logopt ,int facility)    ;
 The string specified by the //ident parameter is appended to the date and time of the log message and is usually set to the name of the program
 
 // The logopt parameter configures the behavior of subsequent syslog calls
 #define LOG_PID		0x01	// Include program PID in log message
 #define LOG_CONS	0x02	// If the message cannot be recorded in the log file, it is printed to the terminal
 #define LOG_ODELAY	0x04	// Delay enabling logging until syslog is called for the first time
 #define LOG_NDELAY	0x08	// Enable logging without delay
 
 // Set the syslog mask
 #include<syslog.h>
 int setlogmask (int maskpri);
 The maskpri parameter specifies the log mask value. This function always succeeds by returning the log mask value from the previous calling process
 
 // Disable logging
 #include<syslog.h>
 void closelog(a); ### user info c++// Used to get and set the real user ID (UID), valid user ID (EUID), real group ID (GID), and valid group ID (EGID) of the current process
#include<sys/types.h>
#include<unistd.h>
uid_t getuid(a);		// Get the real user ID
uid_t geteuid(a);	// Get a valid user ID
gid_t getgid(a);		// Obtain the real group ID
gid_t getegid(a);	// Get the valid group ID
int setuid(uid_t uid);// Set the real user ID
int seteuid(uid_t uid);// Set a valid user ID
int setgid(gid_t gid);// Set the real group ID
int setegid (gid_t gid);// Set the valid group ID
Copy the code

A process has two user ids: UID and EUID. The purpose of EUID is to facilitate resource access: It enables the user running the program to have the privileges of a valid user of the program

Interprocess relationship

Process group

 #include<unistd.h>
 pid_t getgid (pid_t pid);
 // The PGID of the process group to which the pid belongs is returned on success. On failure, -1 is returned and errno is set** Each process has a leader process with the same PGID and PID. The process will persist until all other processes exit or join another process group ** #### session c++// Create a session
 #include<unistd.h>
 pid_t setsid (void);
 // 1. The calling process becomes the leader of the session and is the only member of the new session
 // 2. Create a process group whose PGID is the calling process PID. The calling process becomes the leader of the group
 // 3. The calling process will dump the terminal (if any)
 / / read the SID
 #include<unistd.h>
 pid_t getsid (pid_t pid); #### Interprocess relationship! [Interprocess relationship](HTTPS://i.loli.net/2021/11/21/Lpkyoi73lWF1Obd.png)#### system resource limits c++// All programs running on Linux are affected by resource limitations
#include<sys/resource.h>
int getrlimit (int resource , struct rlimit* rlim);		// Read the resource
int setrlimit (int resource , const struct rlimit* rlim);// Set the resource

/ / rlimit structure
struct rlimit
{
   rlim_t rlim_cur;// Specify a soft limit for the resource
   rlim_t rlim_max;// Specify a hard limit for the resource
}
//rlim_t is an integer type
Copy the code

Change the working directory and root directory

 #include<unistd.h>
 char* getcwd (char* buf,size_t size); // Get the current working directory
 int chdir (const char* path);// Switch the directory specified by path

 // Change the process root function
 #include<unistd.h>
 int chroot (const char* path);
Copy the code

High performance server program framework

I/O processing unit – four I/O models and two efficient event processing modes

Server model

C/S model

Because the client connection request is an asynchronous event that arrives randomly, the server needs to use some KIND of I/O model to listen for this event

** When a connection request is listened for, the server calls the Accept function to accept it and allocates a logical unit to service the new connection. ** ** logical units can be newly created child processes, child threads or other logical units assigned to clients by ** ** servers are child processes created by fork system calls. ** ** the logical unit reads the client request, processes the request, and returns the processing results to the client. ** ** client can continue to send requests to the server after receiving the feedback from the server, or actively close the connection immediately ** ** If the client actively closes the connection, the server passively closes the connection **Copy the code

The server listens for multiple client requests simultaneously through the SELECT system call

The C/S model is very suitable for relatively concentrated resources, and it is simple to implement, but its disadvantages are also obvious, the server is the center, when the traffic is too large, all customers may get a slow response.

P2P model

Advantages: Resources can be fully and freely shared

Disadvantages: When too many requests are transferred between users, the network load increases

Hosts have previously had difficulty discovering each other, so the ACTUAL P2P model used usually has a dedicated discovery server

Server programming framework

The module Single server program Server cluster
I/O processing unit Handle customer connections, read and write network data As an access server, load balancing is implemented
A logical unit Business process or thread Logical server
Network storage unit Local database, file or cache Database server
The request queue The means of communication between units Permanent TCP connection between servers

I/O processing unit module: waits for and accepts new client connections, receives client data, and returns server response data to the client

A logical unit is usually a process or thread: it analyzes and processes customer data, and then passes the results to the I/O processing unit or directly to the client

Network storage unit: you can talk about databases, caches and files, or even a single server

Request queue: Communication between units and abstract I/O processing units that receive a customer request need to be notified in some way to a logical unit to process the request. When multiple logical units access a storage unit at the same time, also need some mechanism to coordinate the processing of race conditions. Request queues are typically implemented as part of a pool. To a server, a request queue is a pre-established, static, permanent TCP connection between servers

I/O model

I/O model Read and write operations and blocking phases
Blocking I/O The program blocks the read-write function
I/O reuse The program blocks on the I/O multiplexing system call, but can listen for multiple I/O events at the same time. The read and write operations on the I/O itself are non-blocking
SIGIO signal The signal triggers a read-ready event, and the user program performs a read/write operation. The program has no blocking phase
Asynchronous I/O The kernel performs a read/write operation and triggers a read/write completion event. The program has no blocking phase

Block type IO

  • Using system calls and blocking until the kernel is ready, and then copying from the kernel buffer to user state, nothing can be done while waiting for the kernel to be ready
  • The following function call blocks until the data is ready and copied from the kernel to the user program. This IO model is blocking IO
  • Blocking IO is the most popular IO model

The advantages and disadvantages

Advantages: simple development, easy to start; During the blocking wait, the user thread hangs, and no CPU resources are consumed during the hang.

Disadvantages: One thread maintains one I/O, which is not suitable for large concurrency. When a large number of concurrent threads are created to maintain network connections, the overhead of memory and threads is very high.

Non-blocking IO

  • The kernel returns an error code when data is not ready, and instead of sleeping, the calling program constantly polls the kernel to see if the data is ready
  • The following function is called if the data is not ready, instead of being blocked like blocking IO, it returns an error code. When the data is ready, the function returns successfully.
  • The application calls such a non-blocking descriptor loop as polling.
  • Polling for non-blocking IO is cpu-intensive and is usually used on a system dedicated to a particular function. You can use this feature by setting non-blocking for the descriptor properties of the socket

The advantages and disadvantages

Advantages of synchronous non-blocking I/O: Each I/O call can be returned immediately while the kernel is waiting for data, and the user thread will not be blocked, resulting in good real-time performance.

Disadvantages of synchronous non-blocking I/O: Multiple threads constantly poll the kernel for data, which consumes a lot of CPU time and is inefficient. Typical Web servers do not adopt this pattern.

Multiplexing IO

  • Similar to non-blocking, except that polling is not performed by the user thread, but by the kernel. When the kernel listener listens to the data, it calls the kernel function to copy the data to the user state
  • The select system call, which acts as the proxy class, polls all the file descriptors registered with it that require IO. When it has results, it tells the recvFROM function that it wants to fetch the data itself
  • IO multiplexing has at least two system calls. If there is only one proxy object, the performance is not as good as the previous IO model, but it is better because it can listen on many sockets at the same time

  • Multiplexing includes:

    • Select: Linearly scans all listened file descriptors, whether they are active or not. Maximum number limit (1024 for 32-bit systems, 2048 for 64-bit systems)
    • Poll: The same as select, but with a different data structure, a PollFD array is allocated and maintained in the kernel. It has no size limit, but requires a lot of copying
    • Epoll: Used in place of poll and select. There is no size limit. Manage multiple file descriptors using a single file descriptor and use red-black tree storage. It also replaces polling with event-driven. The file descriptor registered in epoll_ctl is activated by the callback mechanism when the event is triggered. Epoll_wait is notified. Finally, EPoll also uses MMAP virtual memory mapping technology to reduce the overhead of user – and kernel-mode data transmission

The advantages and disadvantages

Advantages of IO multiplexing: the system does not need to create and maintain a large number of threads, only one thread, a selector can handle thousands of connections at the same time, greatly reducing the system overhead.

Disadvantages of I/O multiplexing: Essentially, select/epoll system calls are blocking, synchronous I/O, and require the system call to block reads and writes after the read and write events are ready.

Signal-driven IO

  • Using signals, the kernel notifies when data is ready
  • First turn on the signal-driven IO socket and use the SIGAction system call to install the signal handler. The kernel returns directly without blocking the user mode
  • When the data is ready, the kernel sends a SIGIO signal, which starts IO operations

Asynchronous I/o

  • Asynchronous IO relies on signal handlers for notifications
  • The difference between the asynchronous I/O model and the previous one is that both blocking and non-blocking are performed in the data preparation phase. The asynchronous I/O model notifies the completion of the I/O operation rather than the completion of data preparation
  • Asynchronous IO is truly non-blocking, with the main process doing its own thing and processing the data through callback functions when the IO operation is complete (data is successfully copied from the kernel cache to the application buffer)
  • Asynchronous IO functions in Unix start with aio_ or lio_

Asynchronous IO benefits: True asynchronous non-blocking, throughput is the highest of these modes.

Disadvantages of asynchronous IO: Applications only need to register and receive events, and the rest work is left to the operating system kernel, so the kernel needs to provide support. In Linux, asynchronous IO was introduced in 2.6 and is not yet fully developed. The underlying implementation still uses Epoll, which is the same as IO multiplexing, so there is no significant performance advantage

Comparison of five IO models

  • The main difference between the previous four IO models is in phase 1, and they are the same in phase 2: data is blocked during copying from the kernel buffer to the caller buffer!
  • The first four types of IO are synchronous IO: the IO operation causes the requestor process to block until the IO operation completes
  • Asynchronous I/O: THE I/O operation does not block the request process

The above I/O model is explained in detail from the network

Two efficient event handling modes

Reactor and Proactor correspond to synchronous AND asynchronous I/O models respectively

Reactor model

It requires that the main thread (the I/O processing unit) only listens for an event on the file description and notifies the worker thread (the logical unit) immediately if it does. Beyond that, the main thread doesn’t do any real work. —– Reading and writing data, accepting new connections, and processing customer requests are all done on the worker thread 1. Read ready events on registered sockets in the main epoll kernel event table 2. The main thread calls epoll_wait to wait for data to be read from the socket 3. Epoll_wait notifies the main thread when there is data to read on the socket. The main thread puts socket-readable events into the request queue 4. Sleep is woken up by a worker thread on the request queue, which reads data from the socket, processes the client request, and registers write-ready events on that socket in the Epoll kernel event table 5. The main thread calls epoll_wait to wait for the socket to write 6. Epoll_wait notifies the main thread when the socket is writable. The main thread queues socket-writable events into the request 7. Sleep is awakened by a worker thread on the request queue, which writes to the socket the result of the server processing the client’s request

Proactor pattern

The Proactor pattern hands all I/O operations over to the main thread and the kernel, leaving the worker thread solely responsible for the business logic 1. The main thread calls aio_read to register read completion events on the socket with the kernel, telling the kernel the location of the read buffer, and how to notify the application when the read operation is complete 2. The main thread continues to process other logic 3. When data on the socket is read into the user buffer, the kernel sends a signal to the application notifying it that the data is available 4. The application’s pre-defined signal handler selects a worker thread to process the customer request. After processing the client request, the worker thread calls aio_write to register the write completion event on the socket with the kernel, and tells the kernel where the write buffer is located, and how to notify the application when the write operation is complete 5. The main thread continues to process other logic 6. When the user buffer’s data is written to the socket, the kernel sends a signal to the application notifying it that the data and sending are complete 7. The application’s pre-defined signal handler selects a worker thread to do the post-processing, such as deciding whether to close the socket

The synchronous I/O model simulates Proactor

The main thread performs data reads and writes, and when the read is complete, the main thread notifies the worker thread of this “completion event.” From the worker thread’s point of view, they get the result of reading and writing data directly, and all they need to do is logically process the read and write operations 1. The main thread registers read ready events on the socket in the epoll kernel event table 2. The main thread calls epoll_wait to wait for data to be read from the socket 3. Epoll_wait notifies the main thread when there is data to read on the socket. The main thread loops through the socket until there is no more data to read, then encapsulates the data into a request object and inserts it into the request queue 4. Sleep is woken up by a worker thread on the request queue, which acquires the request object, processes the customer request, and registers write-ready events on the socket in the epoll kernel event table 5. The main thread calls epoll_wait to wait for the socket to write 6. Epoll_wait notifies the main thread when the socket is writable. The main thread writes to the socket the result of the server processing the client request

Two efficient concurrency models

The concurrency model refers to the method of coordinating tasks between I/O processing units and multiple logical units. Two concurrent programming modes ——- semi-synchronous/semi-asynchronous and leader/follower

Semi-synchronous/semi-asynchronous mode

This synchronization and asynchrony is completely different from the synchronization and asynchrony in the previous I/O model.

* * in the I/O model, "synchronization" and "asynchronous" distinction is the kernel to the application of notification is what kind of I/O events (be ready or completed events), and who will do the I/O read and write (application or kernel) * * * * in concurrent mode, "synchronization" refers to the process to complete in accordance with the code sequence in the order: "Asynchronous" means that program execution needs to be driven by system events **Copy the code

Semi-synchronous/semi-asynchronous workflows

Semi-synchronous/semi-asynchronous mode variants —— semi-synchronous/semi-asynchronous reactor

! [synchronous _ half an asynchronous reactor model] (https://i.loli.net/2021/11/21/fNIhLvi9QgYPr1k.png) * * asynchronous thread only one, as by the main thread, it is responsible for monitoring all the events on the socket. If a readable event occurs on the listening socket ------ a new connection request arrives, the main thread accepts the new connection socket and registers the read/write event on that socket in the epoll kernel event table. If a read/write event occurs on the connected socket ---- a new client request arrives or data is sent to the client, the main thread inserts the connected socket into the request queue. All worker threads sleep on the request queue, and when a task arrives, they compete (for example, by requesting a mutex) to take over the task. This kind of competition allows only idle worker threads to work on new tasks, which makes senseCopy the code

Disadvantages:

** The main thread and the worker thread share the request queue. The main thread adds a task to the request queue, or the worker thread removes a task from the request queue, requiring lock protection on the request queue and wasting CPU time. ** ** Each worker thread can process only one customer request at a time. If there are a large number of clients and a small number of worker threads, the request queue will accumulate many task objects, and the response time of the client will become slower and slower. If you solve this problem by adding worker threads, switching worker threads can also be CPU consuming **Copy the code

The variant —- is relatively efficient

The main thread only manages listener sockets; the connection socket is managed by the worker thread. When a new connection arrives, the main thread accepts and sends the newly returned connection socket to a worker thread. Thereafter, any I/O operations on the new socket are handled by the selected worker thread until the client closes the connection. The easiest way for the main thread to issue a socket to a worker thread is to write data to the pipe between it and the worker thread. When the worker thread detects that there is data readable on the pipe, it analyzes whether a new customer connection request has arrived. If so, it registers read and write events on the new socket in its own EPool kernel event table

The leader/follower model

The ** leader/follower pattern is a pattern in which multiple worker threads take turns acquiring a collection of event sources, and take turns listening, distributing, and processing events. At any point in time, the program has only one leader thread, which listens for I/O events. Other threads are followers, dormant in the thread pool, waiting to become the new leader. If the current leader detects an I/O event, it first elects a new leader thread from the thread pool and then processes the I/O event. At this point, the new leader waits for the new I/O event, while the original leader processes the I/O event, and both realize concurrency including: handle set, thread set, event handler, and specific event handler **! [leader follower model component] (https://i.loli.net/2021/11/21/aGkA7obLFqreN1T.png) * * wait_for_event method is used to monitor these handles on the I/O events, ** Threads in a thread set must be in one of the following ** states at any ** time: ** **Leader: the thread is currently the Leader and is responsible for waiting for I/O events on the handle set ** **Processing: the thread is Processing events. When the leader detects an I/O event, he can move to the processing state to process the event and call the promote_new_leader method to elect a new leader. He can also designate other followers to handle the event, with the leader's status unchanged. If there is no leader in the current thread set, it will become a new leader. Otherwise, it will become a Follower ** **Follower: The thread is currently a Follower and waits to become a new leader by calling the thread set dejoin method. You may also be assigned new tasks by your current leader. [leader follower state transition] (https://i.loli.net/2021/11/21/fCvItE314wT62qU.png) * * * * event handlers and specific event handler. [leader follower workflow] (https://i.loli.net/2021/11/21/vzopABXsTq8xLnl.png) > chart for work processCopy the code

An efficient programming method within a logical unit ——– finite state machines

Other means to improve server performance

Memory pools, process pools, thread pools, and connection pools avoid unnecessary copying, such as the use of shared memory, zero copy, and the use of context switches (thread switches) and locks, which add overhead

Multiprocess programming

Fork system call

A system used to create new processes under Linux

    #include<sys/types.h>
    #include<unistd.h>
    pid_t fork(void);
    // Each call to this function returns the PID of the child twice, in the parent process and in the child process 0. This return value is used by subsequent code to determine whether the current process is a parent or child. Return -1 when the fork call fails and set errno.
Copy the code

The fork function copies the current process and creates a new table entry in the kernel table. The new process table entry has many of the same properties as the original process, such as the heap pointer, stack pointer, and flag register values. However, many attributes are given new values, such as the PPID of the process is set to the PID of the original process, and the signal bitmap is clear (the signal processing function set by the original process no longer applies to the new process).

  • The child has exactly the same code as the parent, and it copies the parent’s data (heap data, stack data, and static data). Data is replicated using what is known as write-on-copy, that is, replication occurs only when either process (parent or child) writes to the data (displays a page miss interrupt, and then the operating system allocates memory to the child and copies the parent’s data). Even so, if we allocate a lot of memory in our program, we should be careful when using forks to avoid unnecessary memory allocation and data replication. After a process is created, file descriptors that are open in the parent process are also open in the child process by default, and the reference count of the file descriptor is increased by one. The reference count of variables such as the user root directory of the parent process and the current working directory is increased by one.

Exec series of system calls

    #include<unistd.h>
    extern char** environ;
    
    int execl(const char* path,const char* argv,...);
    int execlp(const char* file,const char* arg, ...);
    int execle(const char* path,const char* arg, ... .char* const envp[]);
    int execv(const char* path,char* const argv[]);
    int execvp(const char* file,char* const argv[]);
    int execve(const char* path,char* const argv[],char* const envp[]);
    The path argument specifies the full path to the executable file. The file argument accepts the file name. The location of the file is searched in the environment variable path. Arg takes mutable arguments, argv takes an array of arguments, and they are passed to the main function of the new program (the program specified by path or file). The ENvp argument is used to set the environment variables of the new program. If it is not set, the new program uses the environment variable specified by the global environ variable
    // Return -1 on an error and set errno. If nothing goes wrong, the code in the source program will not execute after the exec call, because the source program will have been completely replaced (both code and data) by the program specified by the exec arguments
Copy the code

The exec function does not close a file descriptor opened by the original program unless the file descriptor is set with properties like SOCK_CLOEXEC

Handling zombie processes

For multi-process programs, the parent process typically needs to track the exit status of the child process. Therefore, when the child process finishes running, the kernel does not immediately release the process table entry of the child process to satisfy the parent process’s subsequent query for the child process exit information (if the parent process is still running). The child process is said to continue running after it finishes running and until the parent process reads its exit status. The PPID of the child process is set to 1 by the operating system, that is, the init process. The init process takes over the child process and waits for it to finish. After the parent process exits, the child process is in zombie state until it exits.

    // Zombie mode occupies kernel resources, so use the following functions to wait for the end of the child process and get the return message from the child process to avoid zombie process, or to make the child process zombie state immediately
    #include<sys/types.h>
    #incldue<sys.wait.h>
    pid_t wait(int* stat_loc);
    // The wait function blocks a process until one of its children finishes running, returns the PID of the finished child, and stores its exit status in the memory pointed to by the stat_loc parameter. Several macros are defined in the sys/wait.h header to help interpret the exit status information for the child process
    pid_t waitpid(pid_t pid,int* stat_loc,int options);
    // The waitPID function only waits for the child process specified by the PID parameter. If pid is -1, it is the same as the wait function, which waits for any child process to terminate. The stat_loc parameter has the same meaning as the stat_loc parameter of the wait function, and the options parameter controls the behavior of the waitpid function
    // The WNOHANG waitPID call will be non-blocking, and the target process will return 0 immediately before it finishes, PID if it exits normally, -1 on failure, and errno
Copy the code

It is common to call waitPID in the SIGCHLD signal and terminate a child process completely in the loop

The pipe

Pipes are a common means of communication between parent and child processes. A pipe can pass data between parent and child processes by keeping both pipe file descriptors (fd[0] and fd[1]) open after a fork call. A stack of such file descriptors can only guarantee data transfer in one direction between parent and child processes. The replication process must have one closed fd[0] and the other closed fd[1]—- so two pipes must be used. Socket programming provides a system call for a double-working pipe: Socketpair. ——— can only be used for two related processes (such as parent and child processes)

System IPC

These three methods are used to communicate between unrelated processes: signals, shared memory, and message queues

A semaphore
** When multiple processes access a resource on the system, process synchronization needs to be considered to ensure that only one process can have exclusive access to the resource at any time ---- We call the code accessing the shared resource as critical code, i.e. critical section. **Copy the code

I have more original articles in the public number, welcome to pay attention to, support the original!