Since the last post about the update of the solution to high CPU usage, I have recently received some background comments from enthusiastic viewers, with some questions focusing on CPU switching.

In C/C++, go development, you will pay special attention to memory and CPU usage, those who are particularly concerned about CPU usage, or performance, can check out this article, I believe that after reading the examples at the end, you can help optimize your CPU resource usage.

We all know that CPU context switching increases system load. So what is CPU context and why switch?

What is CPU switching

As we all know, Linux is a multitasking operating system that allows tasks far larger than the number of cpus to run simultaneously. Of course, these tasks are not actually running at the same time, but because the system allocates cpus to them in turn for a very short period of time, creating the illusion of multitasking.

Before each task is executed, the CPU needs to know where the task is loaded from and where it is started. In other words, the system needs to set up the CPU register and Program Counter (PC) for it in advance.

A CPU register is a small but fast memory built into the CPU. The program counter, on the other hand, is used to store the location of the instruction that the CPU is executing, or the location of the next instruction that will be executed. These are the environments that the CPU must depend on before running any task, and are therefore also called CPU contexts.

The saved context is stored in the system kernel and reloaded when the task is rescheduled. This ensures that the original state of the task remains intact and the task appears to be running continuously.

According to different tasks, CPU context switch can be divided into different scenarios, that is, process context switch, thread context switch, interrupt context switch.

Process context switch

Linux divides the running space of processes into kernel space and user space according to privilege levels, which correspond to Ring 0 and Ring 3 of CPU privilege levels in the figure below.

  • Kernel space (Ring 0) has the highest privileges and can directly access all resources;

  • User space (Ring 3) can only access restricted resources, not hardware devices such as memory directly, and must be trapped in the kernel through system calls to access these privileged resources.

Put another way, that is, processes can run in both user space and kernel space. When a process is running in user space, it is called the user state of the process, and when it is trapped in kernel space, it is called the kernel state of the process.

The transition from user mode to kernel mode is accomplished by system call. For example, when we look at the contents of a file, it takes multiple system calls: first open() to open the file, then read() to read the file, then write() to write the content to standard output, and finally close() to close the file.

So, does the system call process switch CPU context? The answer, of course, is yes.

The original user mode instruction location in the CPU register needs to be saved first. Then, in order to execute the kernel code, the CPU register needs to be updated to the new location of the kernel instruction. The last step is to jump to kernel mode and run the kernel task. After the system call, the CPU register needs to be restored to the state saved by the original user, and then switch to user space to continue running the process. So, in the process of one system call, there are actually two CPU context switches. However, it should be noted that the system call does not involve user resources such as virtual memory, nor does it switch processes. This is different from what we would normally call a process context switch:

  • A process context switch is a process that is switched from one process to another.

  • The same process is always running during a system call.

Therefore, the system call process is often referred to as a privileged mode switch rather than a context switch. But in fact, the CPU context switch is unavoidable during system call.

So what’s the difference between a process context switch and a system call?

First, you need to know that processes are managed and scheduled by the kernel, and process switching can only happen in kernel mode. Therefore, the process context includes not only user space resources such as virtual memory, stack, global variables, but also the state of kernel space such as stack and register.

Therefore, the process context switch is one more step than the system call: before saving the current process’s kernel state and CPU registers, the virtual memory, stack, and so on need to be saved. After loading the kernel state of the next process, the virtual memory and user stack of the process need to be refreshed.

As shown in the figure below, the process of saving and restoring context is not “free” and requires the kernel to run on the CPU.

According to test reports, each context switch took tens of nanoseconds to microseconds of CPU time. This time is still considerable, especially in the case of a lot of process context switching, it is easy to cause the CPU to spend a lot of time on registers, kernel stack, virtual memory and other resources to save and restore, and then greatly shorten the actual running process time. This is an important factor in the increase in average load that we discussed in the previous section.

In addition, we know that Linux uses TLB(Translation Lookaside Buffer) to manage the mapping between virtual memory and physical memory. When the virtual memory is updated, the TLB also needs to be refreshed, slowing down memory access. Especially on multiprocessor systems, where the cache is shared by multiple processors, flushing the cache will affect not only the processes of the current processor, but also those of other processors that share the cache.

Now that we know the potential performance problems of process context switching, let’s look at exactly when process context switching happens.

Obviously, context switching is only necessary when the process is switched; in other words, context switching is only necessary when the process is scheduled. Linux maintains a ready queue for each CPU, sorting the active processes (that is, the processes that are running and waiting for the CPU) by priority and the amount of time they have waited for the CPU, and then selecting the processes that need the most CPU, that is, the processes that have the highest priority and wait for the CPU the most.

When will a process be scheduled to run on the CPU?

One of the easiest times to think about is when a process terminates, its CPU is freed, and a new process is taken from the ready queue to run. In fact, there are many other scenarios that trigger process scheduling, and I’ll sort them out for you here.

First, to ensure that all processes are scheduled fairly, CPU time is divided into time slices, which are then allocated to each process in turn. In this way, when a process runs out of time, it is suspended by the system and switched to another process that is waiting for the CPU.

Second, when the system resources are insufficient (such as insufficient memory), the process can not run until the resources are sufficient. At this time, the process will also be suspended and the system will schedule other processes to run.

Third, when a process actively suspends itself via a method such as sleep, it will reschedule.

Fourth, when a process with a higher priority runs, the current process will be suspended and run by the process with a higher priority to ensure the running of the process with a higher priority.

Finally, when a hardware interrupt occurs, processes on the CPU are suspended by the interrupt and run interrupt service routines in the kernel.

It’s important to understand these scenarios, because they are the ones behind any performance issues with context switching.

Thread context switch

Having said that, let’s look at thread-related issues.

The main difference between threads and processes is that threads are the basic unit of scheduling, while processes are the basic unit of resource ownership. To put it bluntly, the so-called task scheduling in the kernel is actually scheduled by threads; The process only provides virtual memory, global variables and other resources to the thread. So, threads and processes can be understood as follows:

  • When a process has only one thread, it can be considered equal to a thread.

  • When a process has multiple threads, these threads share resources such as virtual memory and global variables. These resources do not need to be modified during context switching.

  • In addition, threads also have their own private data, such as stacks and registers, which need to be saved during context switches.

Thus, thread context switching can be divided into two cases:

In the first case, the two threads belong to different processes. At this point, since resources are not shared, the switching process is the same as a process context switch.

In the second case, both threads belong to the same process. At this point, because the virtual memory is shared, so during the switch, the virtual memory resources remain unchanged, only need to switch the thread private data, registers and other non-shared data.

As you can see, switching between threads in the same process consumes less resources than switching between multiple processes, which is an advantage of multi-threading instead of multi-process.

Interrupt context switch

In order to quickly respond to hardware events, the interrupt interrupts the normal scheduling and execution of the interrupt process, and instead invokes the interrupt handler to respond to device events. When you interrupt another process, you need to save the current state of the process so that after the interruption, the process can resume running from its original state.

Interrupt processing has higher priority than process for the same CPU, so interrupt context switching does not occur at the same time as process context switching. Similarly, since interrupts interrupt the scheduling and execution of normal processes, most interrupt handlers are short and concise in order to finish execution as quickly as possible.

In addition, like the process context switch, interrupt context switch also needs CPU consumption, too many switching times will consume a lot of CPU, or even seriously reduce the overall performance of the system. So, when you find too many outages, you need to be careful to see if it can cause serious performance problems for your system.

Process/thread CPU affinity

What is CPU affinity

CPU affinity, the tendency of processes to run on a given CPU for as long as possible without being migrated to other processors. Low process migration frequency means low load. Affinity is a translation of affinity and can actually be called CPU binding.

In multicore machines running, each CPU itself cache, he would have been in the cache with the data used in the process, and no binding CPU, processes may be operating system scheduling to other cpus, so the CPU cache (cache hit ratio is low, that is transferred to the CPU cache area without such data, Load data from memory or hard disk into cache first. When the cache is bound to the CPU, the program will always be executed on the specified CPU and will not be scheduled to other cpus by the operating system, which will improve the performance to some extent.

Another way to use CPU binding is to isolate key processes. If some real-time processes have higher scheduling priorities, they can be bound to a specific CPU core to ensure the scheduling of real-time processes and prevent other PROCESSES from being interfered by this real-time process.

CPU cores can be manually allocated without occupying too much of the same CPU, so CPU affinity can improve performance for certain programs.

CPU affinity can be divided into two broad categories: soft affinity and hard affinity.

The Linux kernel process scheduler is born with a feature called CPU soft affinity, which means that processes don’t usually migrate frequently between processors. This state is exactly what we want, because less frequent process migration means less load. But that doesn’t mean there won’t be small-scale migration.

CPU hard affinity refers to the interface provided by Linux for CPU affinity Settings, which specifies that a process is running on a fixed processor. The CPU affinity mentioned in this article mainly refers to the hard affinity.

The benefits of using CPU affinity

At present, the mainstream server configuration is based on the SMP architecture. In the SMP environment, each CPU has its own cache, which caches the information used by the process, and the process may be scheduled to other cpus by the kernel (the so-called core migration). In this way, the CPU cache hit ratio is low. If CPU affinity is set, the program will always run on the specified CPU, preventing core migration in a multi-SMP environment and avoiding CPU L1/L2 cache failure due to switchover. This further improves application performance.

Use of Linux CPU affinity

There are two ways to specify CPU affinity by which a program runs.

  1. Specify the CPU on which the process will run using the Taskset tool provided by Linux.

  2. Method two, glibc itself provides such an interface, and the borrowed material mainly shows you how to programmatically set the CPU affinity of a process.

Bind processes to run on CPU cores

  • Check how many cores the CPU has

Run cat /proc/cpuinfo to view CPU information

The following two messages:

  • Processor: specifies the number of CPU processors

  • CPU cores, which indicate the number of cores per processor

The number of CPU cores can also be obtained using the system call sysconf:

#include <unistd.h>int sysconf(_SC_NPROCESSORS_CONF); Int sysconf(_SC_NPROCESSORS_ONLN); int sysconf(_SC_NPROCESSORS_ONLN); int sysconf(_SC_NPROCESSORS_ONLN); */#include <sys/sysinfo.h>int get_nprocs_conf (void); /* Number of available cores */int get_nprocs (void); /* Truly reflects the current number of available cores */Copy the code

Use the Taskset directive

  • Obtaining process PID

  • Check which CPU the process is currently running on

The decimal number 3 shown is converted to base 2 with the lowest two 1s, each of which corresponds to a CPU, where the f represents a random assignment.

Specifies that the process is running on CPU1

Note that the CPU label starts at 0, so CPU1 represents the second CPU (the first CPU is labeled 0).

At this point, bind the application to cpu1 and run it as follows:

Use the sched_setaffinity system call

Sched_setaffinity binds a process to a specific CPU.

#define _GNU_SOURCE /* See feature_test_macros(7) */ #include <sched.h The second argument, cpusetsize, is the length of the number specified for the mask * usually set to sizeof(CPU_set_t) * if pid is 0, it indicates the current process is specified */ int sched_setaffinity(pid_t pid, size_t cpusetsize, cpu_set_t *mask); int sched_getaffinity(pid_t pid, size_t cpusetsize, cpu_set_t *mask); /* Get the CPU bit mask of the process indicated by pid and return the mask to the structure pointed to by mask */Copy the code

  • The instance

    #include

    #include

    #include

    #include

    #include

    #define __USE_GNU#include

    #include

    #include

    #include #define THREAD_MAX_NUM 200 Int num=0; // Maximum number of processes in a CPU int num=0; Void * threadFun(void* arg) {set_t mask; Cpu_set_t get; Int *a = (int *)arg; int i; printf(“the thread is:%d\n”,a); // display the number of threads CPU_ZERO(&mask); / / empty CPU_SET (a, & mask); If (sched_setaffinity(0, sizeof(mask), &mask) == -1)// Set thread CPU affinity {printf(“warning: could not set CPU affinity, continuing… \n”); } CPU_ZERO(&get); If (sched_getaffinity(0, sizeof(get), &get) == -1)// get thread CPU affinity {printf(“warning: cound not get thread affinity, continuing… \n”); } for (i = 0; i < num; Printf (“this thread %d is running processor: %d\n”, I, I); printf(“this thread %d is running processor: %d\n”, I, I); } } return NULL; }int main(int argc, char argv[]){ int tid[THREAD_MAX_NUM]; int i; pthread_t thread[THREAD_MAX_NUM]; num = sysconf(_SC_NPROCESSORS_CONF); If (num > THREAD_MAX_NUM) {printf(“num of cores[%d] is bigger than THREAD_MAX_NUM[%d]! \n”, num, THREAD_MAX_NUM); return -1; } printf(“system has %i processor(s). \n”, num); for(i=0; i







  • Bind threads to run on the CPU core

Bind threads to CPU cores using the pthread_setaffinity_NP function, whose prototype is defined as follows:

#define _GNU_SOURCE             /* See feature_test_macros(7) */
#include <pthread.h>
int pthread_setaffinity_np(pthread_t thread, size_t cpusetsize,
const cpu_set_t *cpuset);
int pthread_getaffinity_np(pthread_t thread, size_t cpusetsize,
 cpu_set_t *cpuset);
Compile and link with -pthread.
Copy the code

  • The parameters have meanings similar to sched_setaffinity.

  • The instance

    #define _GNU_SOURCE#include #include

    #include

    #include

    #define handle_error_en(en, msg) \ do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)intmain(int argc, char argv[]){ int s, j; cpu_set_t cpuset; pthread_t thread; thread = pthread_self(); / Set affinity mask to include CPUs 0 to 7 / CPU_ZERO(&cpuset); for (j = 0; j < 8; j++) CPU_SET(j, &cpuset); s = pthread_setaffinity_np(thread, sizeof(cpu_set_t), &cpuset); if (s ! = 0) handle_error_en(s, “pthread_setaffinity_np”); / Check the actual affinity mask assigned to the thread */ s = pthread_getaffinity_np(thread, sizeof(cpu_set_t), &cpuset); if (s ! = 0) handle_error_en(s, “pthread_getaffinity_np”); printf(“Set returned by pthread_getaffinity_np() contained:\n”); for (j = 0; j < CPU_SETSIZE; j++) if (CPU_ISSET(j, &cpuset)) printf(” CPU %d\n”, j); exit(EXIT_SUCCESS); }


  • With affinity binding, you can use mpstat -p ALL 1 to view the usage of each CPU core. The parameters are not explained here (you can understand the parameters in English).

conclusion

Now that you know how to use CPU resource binding to reduce performance issues associated with CPU switching, have you ever wondered what the problems might be? What are his strengths and weaknesses? Feel free to leave a comment or comment below