Linux内核学习笔记

一.进程管理

1.进程创建--fork()

fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程(PID、PPID、某些资源和统计量不同),也就是两个进程可以做完全相同的事。

1)在父进程中,fork返回新创建子进程的进程ID;  

2)在子进程中,fork返回0;  

3)如果出现错误,fork返回一个负值;  

在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。

创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。 每个进程都有一个独特(互不相同)的进程标识符(process ID),可以通过getpid()函数获得,还有一个记录父进程pid的变量,可以通过getppid()函数获得变量的值。

两个进程的变量都是独立的,存在不同的地址中,不是共用的

实验代码:

实验现象:

2.进程回收--wait()

在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。但是仍然为其保留一定的信息,这些信息主要指进程控制块 PCB 的信息(进程号、退出状态、运行时间等)

父进程可以通过调用 wait 得到它的退出状态同时彻底清除这个进程。父进程一旦调用了wait就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

注:

  当父进程忘了用wait()函数等待已终止的子进程时,子进程就会进入一种无父进程的状态,此时子进程就是僵尸进程.

  wait()要与fork()配套出现,如果在使用fork()之前调用wait(),wait()的返回值则为-1,正常情况下wait()的返回值为子进程的PID.

  如果先终止父进程,子进程将继续正常进行,只是它将由init进程(PID 1)继承,当子进程终止时,init进程捕获这个状态.

  参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,就像下面这样:

pid = wait(NULL)

使用例子:

3.进程替换--exec ()

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。

当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。

调用exec并不创建新进程,所以调用exec前后该进程的id并未改变

exec函数族的函数执行成功后不会返回,调用失败时,会设置erro并返回-1,然后从原程序的调用点接着往下执行。

实验:

编译运行

在代码中间加入execl后

编译运行后:

在调用excel之后打印出来的内容跟直接调用ls一模一样

execl运行成功以后,原程序的内容便不再执行,我们原始代码里还有一个printf没打印出来

一旦调用成功,后续所有代码都不会再执行

故意把代码写错:

此时execl调用失败。execl函数出错的时候才有返回值,返回值是-1,成功时没有返回值。

4.进程终止

正常终止:

        • 从 main 函数返回。

        • 调用 exit() 函数终止。

        • 调用 _exit() 函数终止

异常终止

        • 调用 abort() 函数异常终止。

        • 由系统信号终止。

 exit() 函数定义在 stdlib.h 中,而 _exit() 定义在 unistd.h 中。二者有一定的区别:

_exit() 函数的作用最为简单:直接通过系统调用使进程终止运行,当然,在终止进程的时候会清除这个进程使用的内存空间,并销毁它在内核中的各种数据结构;而 exit() 函数则在这些基础上做了一些包装,在执行退出之前加了若干道工序:比如 exit() 函数在调用 exit系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,这就是“清除 I/O 缓冲“

这两个函数都会传入一个参数 status,这个参数表示的是进程终止时的状态码,0 表示正常终止,其他非 0 值表示异常终止,一般都可以使用-1 或者 1 表示,标准 C 里有 EXIT_SUCCESS 和EXIT_FAILURE 两个宏,表示正常与异常终止。使用例子如下:

二.进程通信

Linux 内核提供了各种各样的内核对象用于协调进程间的通讯,如信号、管道、消息队列等。进程间的通信主要用于:数据传输、资源共享、事件通知、进程控制。

1.管道

当数据从一个进程连接流到另一个进程时,这之间的连接就是一个管道(pipe)。我们通常是把一个进程的输出通过管道连接到另一个进程的输入。管道分为匿名管道和命名管道。

1.1 匿名管道

  匿名管道最常见的形态就是我们在shell 操作中最常用的“|”。它的特点是只能在父子进程中使用,父进程在

产生子进程前必须打开一个管道文件,然后fork 产生子进程,这样子进程通过拷贝父进程的进程地址空间获得同一个管道文件的描述符,以达到使用同一个管道通信的目的。此时除了父子进程外,没人知道这个管道文件的描述符,所以通过这个管道中的信息无法传递给其他进程。这保证了传输数据的安全性,当然也降低了管道了通用性,于是系统还提供了命名管道,它本质是一个文件,位于文件系统中,命名管道可以让多个无相关的进程进行通讯。

pipe() 函数用于创建一个匿名管道,一个可用于进程间通信的单向数据通道。

#include <unistd.h>

int pipe(int pipefd[2]);

函数原型非常简单,没有任何的传入参数,注意:数组pipefd 是用于返回两个引用管道末端的文件描述符,它是一个由两个文件描述符组成的数组的指针。pipefd[0] 指管道的读取端,pipefd[1]指向管道的写入端,向管道的写入端写入数据将会由内核缓冲,即写入内存中,直到从管道的读取端读取数据为止,而且数据遵循先进先出原则。pipe() 函数还会返回一个int 类型的变量,如果为0 则表示创建匿名管道成功,如果为-1 则表示创建失败,并且设置errno。

匿名管道创建成功以后,创建该匿名管道的进程(父进程)同时掌握着管道的读取端和写入端,但是想要父子进程间有数据交互,则需要以下操作:

• 父进程调用pipe() 函数创建匿名管道,得到两个文件描述符pipefd[0]、pipefd[1],分别指向管道的读取端和写入端。

• 父进程调用fork() 函数启动(创建)一个子进程,那么子进程将从父进程中继承这两个文件描述pipefd[0]、pipefd[1],它们指向同一匿名管道的读取端与写入端。

• 由于匿名管道是利用环形队列实现的,数据将从写入端流入管道,从读取端流出,这样子就实现了进程间通信,但是这个匿名管道此时有两个读取端与两个写入端,因此需要进行接下来的操作:

• 如果想要从父进程将数据传递给子进程,则父进程需要关闭读取端,子进程关闭写入端,如图数据从父进程流向子进程所示。

• 如果想要从子进程将数据传递给父进程,则父进程需要关闭写入端,子进程关闭读取端,如图数据从子进程流向父进程所示。

• 当不需要管道的时候,就在进程中将未关闭的一端关闭即可。

示例代码如下:

#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_DATA_LEN 256
#define DELAY_TIME 1

int main()
{
    pid_t pid;
    int pipe_fd[2];
    char buf[MAX_DATA_LEN];
    const char data[] = "Pipe Test Program";
    int real_read, real_write;

    memset((void*)buf, 0, sizeof(buf));

    /* 创建管道 */
    if (pipe(pipe_fd) < 0)
    {
        printf("pipe create error\n");
        exit(1);
    }

    /* 创建一子进程 */
    if ((pid = fork()) == 0)
    {
        /* 子进程关闭写描述符,并通过使子进程暂停 3s 等待父进程已关闭相应的读描述符 */
        close(pipe_fd[1]);
        sleep(DELAY_TIME * 3);

        /* 子进程读取管道内容  没有内容可读的话 就一直阻塞*/
        if ((real_read = read(pipe_fd[0], buf, MAX_DATA_LEN)) > 0)
        {
            printf("%d bytes read from the pipe is '%s'\n", real_read, buf);
        }

        /* 关闭子进程读描述符 */
        close(pipe_fd[0]);

        exit(0);
    }
    
    else if (pid > 0)
    {
        /* 父进程关闭读描述符,并通过使父进程暂停 1s 等待子进程已关闭相应的写描述符 */
        close(pipe_fd[0]);

        sleep(DELAY_TIME);
        
        if((real_write = write(pipe_fd[1], data, strlen(data))) != -1)
        {
            printf("Parent write %d bytes : '%s'\n", real_write, data);
        }
        
        /*关闭父进程写描述符*/
        close(pipe_fd[1]);

        /*收集子进程退出信息*/
        waitpid(pid, NULL, 0);

        exit(0);
    }
}

1.2有名管道

如果想在不相关的进程之间交换数据,我们可以用FIFO 文件来完成这项工作,或者称之为命名管道。命名管道是一种特殊类型的文件,它在文件系统中以文件名的形式存在,但它的的数据却是存储在内存中的。我们可以在终端(命令行)上创建命名管道,也可以在程序中创建它。

我们可以通过调用mkfifo函数创建一个命名管道,其实就类似于创建一个文件,只不过这个文件的类型是命名管道的类型。

函数原型如下:

int mkfifo(const char * pathname,mode_t mode);

它所依赖的头文件:

#include <sys/types.h>

#include <sys/stat.h>

mkfifo() 会根据参数pathname(也就是有名管道的管道名称) 建立特殊的FIFO 文件,而参数mode 为该文件的模式与权限。mkfifo() 创建的FIFO 文件其他进程都可以进行读写操作,可以使用读写一般文件的方式操作它,如open,read,write,close 等。

mode 模式及权限参数说明:

函数返回值说明如下:

示例代码:

#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <string.h>


#define MYFIFO "myfifo"    /* 有名管道文件名*/

#define MAX_BUFFER_SIZE PIPE_BUF /* 4096 定义在于 limits.h 中*/


void fifo_read(void)
{
    char buff[MAX_BUFFER_SIZE];
    int fd;
    int nread;

    printf("***************** read fifo ************************\n");
    /* 判断有名管道是否已存在,若尚未创建,则以相应的权限创建*/
    if (access(MYFIFO, F_OK) == -1)
    {
        if ((mkfifo(MYFIFO, 0666) < 0) && (errno != EEXIST))
        {
            printf("Cannot create fifo file\n");
            exit(1);
        }
    }

    /* 以只读阻塞方式打开有名管道 */
    fd = open(MYFIFO, O_RDONLY);
    if (fd == -1)
    {
        printf("Open fifo file error\n");
        exit(1);
    }

    memset(buff, 0, sizeof(buff));

    if ((nread = read(fd, buff, MAX_BUFFER_SIZE)) > 0)
    {
        printf("Read '%s' from FIFO\n", buff);
    }

   printf("***************** close fifo ************************\n");

    close(fd);

    exit(0);
}


void write_fifo(void)
{
    int fd;
    char buff[] = "this is a fifo test demo";
    int nwrite;

    sleep(2);   //等待子进程先运行

    /* 以只写阻塞方式打开 FIFO 管道 */
    fd = open(MYFIFO, O_WRONLY | O_CREAT, 0644);
    if (fd == -1)
    {
        printf("Open fifo file error\n");
        exit(1);
    }

    printf("Write '%s' to FIFO\n", buff);
    /*向管道中写入字符串*/
    nwrite = write(fd, buff, MAX_BUFFER_SIZE);
    
    if(wait(NULL))  //等待子进程退出
    {
        close(fd);
        exit(0);
    }

}


int main()
{
    pid_t result;
    /*调用 fork()函数*/
    result = fork();

    /*通过 result 的值来判断 fork()函数的返回情况,首先进行出错处理*/
    if(result == -1)
    {
        printf("Fork error\n");
    }


    else if (result == 0) /*返回值为 0 代表子进程*/
    {
        fifo_read();
    }

    else /*返回值大于 0 代表父进程*/
    {
        write_fifo();
    }

    return result;
}


2.信号

信号(signal),又称为软中断信号,用于通知进程发生了异步事件,它是Linux 系统响应某些条件而产生的一个事件,它是在软件层次上对中断机制的一种模拟,是一种异步通信方式,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。

当进程接收到一个信号的时候,会相应地采取一些行动。我们可以使用术语“生成(raise)”表示一个信号的产生,使用术语“捕获(catch)”表示进程接收到一个信号。

系统支持的信号种类,可以通过 kill -l 查看

一般而言,信号的响应处理过程如下:如果该信号被阻塞,那么将该信号挂起,不对其做任何处理,等到解除对其阻塞为止。如果该信号被捕获,那么进一步判断捕获的类型,如果设置了响应函数,那么执行该响应函数;如果设置为忽略,那么直接丢弃该信号。最后才执行信号的默认处理。

可以将这62 种(32 33 没有)信号分为2 大类:信号值为1~31 的信号属性非实时信号(也称为不可靠信号,这类信号不支持排队,因此信号可能会丢失。比如发送多次相同的信号,进程只能收到一次,也只会处理一次,因此剩下的信号将被丢弃。),它们是从UNIX 系统中继承下来的信号,信号值为34~64 的信号为实时信号(也被称为可靠信号,它是支持排队的,发送了多少个信号给进程,进程就会处理多少次)。

常见的Linux信号及其描述:

SIGHUP:挂断信号。关闭终端时发出。
SIGINT:中断信号。用户按下Ctrl+C时发出。
SIGQUIT:退出信号。用户按下Ctrl+\时发出。
SIGILL:非法指令。进程执行了非法、损坏的指令。
SIGTRAP:跟踪/断点指令。用于调试。
SIGABRT:中止信号。进程调用abort函数时发出。
SIGBUS:总线错误。内存访问出错。
SIGFPE:浮点异常。浮点运算错误。
SIGKILL:杀死信号。不可被捕获、忽略,默认会终止进程。  (kill -9 pid)
SIGUSR1:用户定义信号1。允许用户指定程序中的某个行为。
SIGSEGV:段错误。访问未分配的内存区域。
SIGUSR2:用户定义信号2。允许用户指定程序中的某个行为。
SIGPIPE:管道破裂。进程向一个没有读端的管道写数据。
SIGALRM:闹钟信号。alarm函数设置的定时器时间到。
SIGTERM:结束信号。默认会请求终止进程,可被处理。 (kill pid) 这条命令不如命令9强硬 可能杀不死进程
(已废弃)
SIGCHLD:子进程状态改变。子进程结束、停止或继续时发出。
SIGCONT:继续信号。使暂停的进程继续执行。
SIGSTOP:停止信号。不能被捕获/忽略,只能被处理。
SIGTSTP:暂停信号。用户按下Ctrl+Z时发出,暂停执行。
SIGTTIN:后台输入信号。后台进程尝试读取终端。
SIGTTOU:后台输出信号。后台进程尝试写向终端。
SIGURG:紧急条件。网络套接字有紧急数据。
SIGXCPU:超过CPU限制。
SIGXFSZ:超过文件大小限制。
SIGVTALRM:虚拟时钟信号。setitimer函数设置的时限到达。
SIGPROF:profile时钟信号。
SIGWINCH:窗口大小改变。
SIGIO:输入输出可以进行。
SIGPWR:电源失效。
SIGSYS:无效系统调用。

signal函数

signal() 主要是用于捕获信号,可以改变进程中对信号的默认行为,我们在捕获这个信号后,也可以自定义对信号的处理行为,当收到这个信号后,应该如何去处理它,这也是在开发Linux最常使用的方式。

使用signal() 时,它需要提前设置一个回调函数,即进程接收到信号后将要跳转执行的响应函数,或者设置忽略某个信号,才能改变信号的默认行为,这个过程称为“信号的捕获”。对一个信号的“捕获”可以重复进行,不过signal() 函数将会返回前一次设置的信号响应函数指针。

原型如下:

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

signal 是一个带有signum 和handler 两个参数的函数。准备捕获或忽略的信号由参数signum 指出,接收到指定的信号后将要调用的函数由参数handler 指出。

signum 是指定捕获的信号,如果指定的是一个无效的信号,或者尝试处理的信号是不可捕获或不可忽略的信号(如SIGKILL),errno 将被设置为EINVAL。

handler 是一个函数指针,它的类型是void(*sighandler_t)(int) 类型,拥有一个int 类型的参数,这个参数的作用就是传递收到的信号值,返回类型为void。signal() 函数会返回一个sighandler_t 类型的函数指针,这是因为调用signal() 函数修改了信号的行为,需要返回之前的信号处理行为是哪个,以便让应用层知悉。

handler 需要用户自定义处理信号的方式,当然还可以使用以下宏定义:

• SIG_IGN:忽略该信号。

• SIG_DFL:采用系统默认方式处理信号。

示例代码:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>

/** 信号处理函数 */
void signal_handler(int sig)
{
    printf("\nthis signal number is %d \n",sig);

    if (sig == SIGINT) {
        printf("I have get SIGINT!\n\n");
        printf("The signal has been restored to the default processing mode!\n\n");
        /** 恢复信号为默认情况 */
        signal(SIGINT, SIG_DFL);
    }

}

int main(void)
{
    printf("\nthis is an singal test function\n\n");
    /** 设置信号处理的回调函数 */
    signal(SIGINT, signal_handler);
    while (1) {
        printf("waiting for the SIGINT signal , please enter \"ctrl + c\"...\n");
        sleep(1);
    }

    exit(0);
}
 

kill函数

kill 系统命令只是kill() 函数的一个用户接口。这里需要注意的是,它不仅可以中止进程(实际上发出SIGKILL 信号),也可以向进程发送其他信号。函数原型如下:

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);

kill() 函数的参数有两个,分别是pid 与sig,还返回一个int 类型的错误码。

• pid 的取值如下:

– pid > 1:将信号sig 发送到进程ID 值为pid 指定的进程。

– pid = 0:信号被发送到所有和当前进程在同一个进程组的进程。

– pid = -1:将sig 发送到系统中所有的进程,但进程1(init)除外。

– pid < -1:将信号sig 发送给进程组号为-pid (pid 绝对值)的每一个进程。

• sig:要发送的信号值。

• 函数返回值:

– 0:发送成功。

– -1:发送失败。

raise函数

raise() 函数也是发送信号函数,不过与kill() 函数所不同的是,raise() 函数只是进程向自身发送信号的,而没有向其他进程发送信号,可以说kill(getpid(),sig) 等同于raise(sig)。

函数原型:

int raise(int sig);

raise() 函数只有一个参数sig,它代表着发送的信号值,如果发送成功则返回0,发送失败则返回-1,

示例代码:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(void)
{
    pid_t pid;

    int ret;

    /* 创建一子进程 */
    if ((pid = fork()) < 0) {
        printf("Fork error\n");
        exit(1);
    }

    if (pid == 0) {
        /* 在子进程中使用 raise()函数发出 SIGSTOP 信号,使子进程暂停 */
        printf("Child(pid : %d) is waiting for any signal\n\n", getpid());

        /** 子进程停在这里 */
        raise(SIGSTOP);

        exit(0);
    }

    else {
        /** 等待一下,等子进程先执行 */
        sleep(1);

        /* 在父进程中收集子进程发出的信号(不阻塞),并调用 kill()函数进行相应的操作 */
        if ((waitpid(pid, NULL, WNOHANG)) == 0) {
            /** 子进程还没退出,返回为0,就发送SIGKILL信号杀死子进程 */
            if ((ret = kill(pid, SIGKILL)) == 0) {
                printf("Parent kill %d\n\n",pid);
            }
        }

        /** 一直阻塞直到子进程退出(杀死) */
        waitpid(pid, NULL, 0);

        exit(0);
    }
}

3.消息队列

消息队列是System V中的一种进程间通信机制(信号量、消息队列、共享内存),消息队列就好比是一个快递柜,发送方发送消息的时候会把要发送的消息放到快递柜中,接收方在方便的时候可以从快递柜中把消息拿出来。在linux系统中,消息队列本质上是内核维护的一片内存。

消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。我们可以通过发送消息来避免命名管道的同步和阻塞问题。

消息队列的实现包括创建或打开消息队列、发送消息、接收消息和控制消息队列这 4 种操作。

消息队列,在系统中使用一种叫做 key 的键值来唯一标识,而且是“持续性”资源——即他们被创建之后,不会因为进程的退出而消失,而会持续地存在,除非调用特殊的函数或者命令删除他们。

通过ipcs命令可以查看系统当前的 IPC 对象

# 查询系统当前的 IPC 对象

ipcs

# 以下是示例输出,没有使用的情况下可能为空

--------- 消息队列 -----------

ftok函数

函数原型:

key_t ftok(const char *pathname, int proj_id);

函数功能:获得项目相关的唯一的IPC键值

函数参数:

pathname:文件路径名(注意用绝对路径)

proj_id:项目ID,非0整数(只有低8位有效)

函数返回值:成功返回key值,失败返回-1。

msgget函数

收发消息前需要具体的消息队列对象,msgget() 函数的作用是创建或获取一个消息队列对象,并返回消息队列标识符。函数原型如下:

int msgget(key_t key, int msgflg);

若执行成功返回队列 ID,失败返回-1。它的两个输入参数说明如下:

key:消息队列的关键字值,多个进程可以通过它访问同一个消息队列。例如收发进程都使用同一个键值即可使用同一个消息队列进行通讯。其中有个特殊值 IPC_PRIVATE,它用于创建当前进程的私有消息队列。

msgflg:表示创建的消息队列的模式标志参数,主要有 IPC_CREAT, IPC_EXCL 和权限 mode,

--如果是 IPC_CREAT 为真表示:如果内核中不存在关键字与 key 相等的消息队列,则新建一个消息队列;如果存在这样的消息队列,返回此消息队列的标识符。

   – 而如果为 IPC_CREAT | IPC_EXCL 表示如果内核中不存在键值与 key 相等的消息队列,则新建一个消息队列;如果存在这样的消息队列则报错。

– mode 指 IPC 对象存取权限,它使用 Linux 文件的数字权限表示方式,如 0600,0666等。

这些参数是可以通过“|”运算符联合起来的,因为它始终是 int 类型的参数。如 msgflag使用参数 IPC_CREAT | 0666 时表示,创建或返回已经存在的消息队列的标识符,且该消息队列的存取权限为 0666,即消息的所有者,所属组用户,其他用户均可对该消息进行读写。

msgsnd函数

这个函数的主要作用就是将消息写入到消息队列,俗称发送一个消息。函数原型如下:

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

参数说明:

• msqid:消息队列标识符。

• msgp:发送给队列的消息。msgp 可以是任何类型的结构体,但第一个字段必须为 long 类型,即表明此发送消息的类型,msgrcv() 函数则根据此接收消息。msgp 定义的参照格式如下:

/*msgp 定义的参照格式 */
struct s_msg{
long type;
/* 必须大于 0, 消息类型 */
char mtext[1];
/* 消息正文,可以是其他任何类型 */
} msgp;

• msgsz:要发送消息的大小,不包含消息类型占用的 4 个字节,即 mtext 的长度。

•msgflg:如果为 0 则表示:当消息队列满时,msgsnd() 函数将会阻塞,直到消息能写进消息队列;如果为 IPC_NOWAIT 则表示:当消息队列已满的时候,msgsnd() 函数不等待立即返回;如果为IPC_NOERROR:若发送的消息大于 size 字节,则把该消息截断,截断部分将被丢弃,且不通知发送进程。

如果成功则返回 0,如果失败则返回-1

msgrcv函数

msgrcv() 函数是从标识符为 msqid 的消息队列读取消息并将消息存储到 msgp 中,读取后把此消息从消息队列中删除,也就是俗话说的接收消息。函数原型:

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

参数说明:

• msqid:消息队列标识符。

• msgp:存放消息的结构体,结构体类型要与 msgsnd() 函数发送的类型相同。

• msgsz:要接收消息的大小,不包含消息类型占用的 4 个字节。

• msgtyp 有多个可选的值:如果为 0 则表示接收第一个消息,如果大于 0 则表示接收类型等于 msgtyp 的第一个消息,而如果小于 0 则表示接收类型等于或者小于 msgtyp 绝对值的第一个消息。

• msgflg 用于设置接收的处理方式,取值情况如下:

– 0: 阻塞式接收消息,没有该类型的消息 msgrcv 函数一直阻塞等待

– IPC_NOWAIT:若在消息队列中并没有相应类型的消息可以接收,则函数立即返回,此时错误码为 ENOMSG

– IPC_EXCEPT:与 msgtype 配合使用返回队列中第一个类型不为 msgtype 的消息

– IPC_NOERROR:如果队列中满足条件的消息内容大于所请求的 size 字节,则把该消息截断,截断部分将被丢弃

返回值:msgrcv() 函数如果接收消息成功则返回实际读取到的消息数据长度,否则返回-1

msgctl函数

消息队列是可以被用户操作的,比如设置或者获取消息队列的相关属性,那么可以通过 msgctl()函数去处理它。函数原型:

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

参数说明:

• msqid:消息队列标识符。

cmd 用于设置使用什么操作命令,它的取值有多个:

– IPC_STAT 获取该 MSG 的信息,获取到的信息会储存在结构体 msqid_ds 类型的 buf 中。

– IPC_SET 设置消息队列的属性,要设置的属性需先存储在结构体 msqid_ds 类型的 buf中,可设置的属性包括: msg_perm.uid、 msg_perm.gid、 msg_perm.mode 以及 msg_qbytes,储存在结构体 msqid_ds 中。

– IPC_RMID 立即删除该 MSG,并且唤醒所有阻塞在该 MSG 上的进程,同时忽略第三个参数。

– IPC_INFO 获得关于当前系统中 MSG 的限制值信息。

– MSG_INFO 获得关于当前系统中 MSG 的相关资源消耗信息。

– MSG_STAT 同 IPC_STAT,但 msgid 为该消息队列在内核中记录所有消息队列信息的数组的下标,因此通过迭代所有的下标可以获得系统中所有消息队列的相关信息。

buf:相关信息结构体缓冲区。

如果成功,返回0. 出错,返回-1.

4.信号量

信号量本质上是一个计数器,用于协调多进程间对共享数据对象的读取,它不以传送数据为主要目的,它主要是用来保护共享资源(信号量也属于临界资源),使得该临界资源在一个时刻只有一个进程独享。

semget函数

semget 函数的功能是创建或者获取一个已经创建的信号量,如果成功则返回对应的信号量标识符,失败则返回-1。函数原型如下:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);

参数说明:

• key:与消息队列一样的是,参数 key 用来标识系统内的信号量,如果指定的 key 已经存在,则意味着打开这个信号量,这时 nsems 参数指定为 0,semflg 参数也指定为 0。特别地,可以使用 IPC_PRIVATE创建一个没有 key 的信号量。

• nsems:本参数用于在创建信号量的时候,表示可用的信号量数目。

• semflg: semflg 参数用来指定标志位,与消息队列中的类似。主要有 IPC_CREAT, IPC_EXCL和权限 mode,其中使用 IPC_CREAT 标志创建新的信号量,即使该信号量已经存在(具有同一个键值的信号量已在系统中存在),也不会出错。如果同时使用 IPC_EXCL 标志可以创建一个新的唯一的信号量,此时如果该信号量已经存在,该函数会返回出错。

创建成功,返回信号量的ID。失败,返回-1.

semctl函数

semctl 函数主要是对信号量集的一系列控制操作,根据操作命令 cmd 的不同,执行不同的操作,第四个参数是可选的。原型如下:

int semctl(int semid, int semnum, int cmd, ...);

•semid:System V 信号量的标识符;

• semnum:表示信号量集中的第 semnum 个信号量。它的取值范围:0 ~ nsems-1 。

• cmd:操作命令,主要有以下命令:

– IPC_STAT:获取此信号量集合的 semid_ds 结构,存放在第四个参数的 buf 中。

– IPC_SET:通过第四个参数的 buf 来设定信号量集相关联的 semid_ds 中信号量集合权限为sem_perm 中的 uid,gid,mode。

– IPC_RMID:从系统中删除该信号量集合。

– GETVAL:返回第 semnum 个信号量的值。

– SETVAL:设置第 semnum 个信号量的值,该值由第四个参数中的 val 指定。

– GETPID:返回第 semnum 个信号量的 sempid,最后一个操作的 pid。

– GETNCNT:返回第 semnum 个信号量的 semncnt。等待 semval 变为大于当前值的线程数。

– GETZCNT:返回第 semnum 个信号量的 semzcnt。等待 semval 变为 0 的线程数。

– GETALL:去信号量集合中所有信号量的值,将结果存放到的 array 所指向的数组。

– SETALL:按 arg.array 所指向的数组中的值,设置集合中所有信号量的值。

• 第四个参数是可选的:如果使用该参数,该参数的类型为 union semun,它是多个特定命令的联合体,具体如下:

union semun {
int val;
/* Value for SETVAL */
struct semid_ds *buf;     /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array;     /* Array for GETALL, SETALL */
struct seminfo *__buf;     /* Buffer for IPC_INFO
                                (Linux-specific) */
};

成功,返回0.否则,-1.

semop函数

Linux 提供了 semop() 函数对信号量进行 PV 操作。函数原型如下:

int semop(int semid, struct sembuf *sops, size_t nsops);

参数说明:

• semid:System V 信号量的标识符,用来标识一个信号量。

• sops:是指向一个 struct sembuf 结构体数组的指针,该数组是一个信号量操作数组。原型如下:

struct sembuf
{
unsigned short int sem_num;    /* 信号量的序号从 0 ~ nsems-1 */
short int sem_op;            /* 对信号量的操作,>0, 0, <0 */
short int sem_flg;                /* 操作标识:0, IPC_WAIT, SEM_UNDO */
};

– sem_num 用于标识信号量中的第几个信号量,0 表示第 1 个,1 表示第 2 个,nsems -1表示最后一个。

– sem_op 标识对信号量的所进行的操作类型。对信号量的操作有三种类型:

  sem_op 大于 0,表示进程对资源使用完毕,交回该资源,即对该信号量执行 V 操作

  sem_op 小于 0,表示进程希望使用资源,对该信号量执行 P 操作

  sem_op 等于 0,表示进程要阻塞等待,直至信号量当前值 semval 变为 0

  – sem_flg,信号量操作的属性标志,可以指定的参数包括 IPC_NOWAIT 和 SEM_UNDO。如果为 0,表示正常操作;当指定了 SEM_UNDO,那么将维护进程对信号量的调整值,进程退出的时候会自动还原它对信号量的操作;当指定了 IPC_WAIT,使对信号量的操作时非阻塞的。即指定了该标志,调用进程在信号量的值不满足条件的情况下不会被阻塞,而是直接返回-1,并将 errno 设置为 EAGAIN。

只有sembuf 结构的 sem_flag 指定为 SEM_UNDO 后,信号量调整值才会随着 sem_op 而更新

•nsops:表示上面 sops 数组的数量,如只有一个 sops 数组,nsops 就设置为 1。

5.共享内存

共享内存就是将内存进行共享,它允许多个不相关的进程访问同一个逻辑内存,直接将一块裸露的内存放在需要数据传输的进程面前,让它们自己使用。因此,共享内存是效率最高的一种 IPC 通信机制,它可以在多个进程之间共享和传递数据,进程间需要共享的数据被放在共享内存区域,所有需要访问该共享区域的进程都要把该共享区域映射到本进程的地址空间中去。

共享内存是属于临界资源,在某一时刻最多只能有一个进程对其操作(读/写数据)共享内存一般不能单独使用,而要配合信号量、互斥锁等协调机制,让各个进程在高效交换数据的同时,不会发生数据践踏、破坏等意外。

shmget函数

内核提供了 shmget() 函数的创建或获取一个共享内存对象,并返回共享内存标识符。函数原型如

下:

int shmget(key_t key, size_t size, int shmflg);

参数说明:

• key:标识共享内存的键值,可以有以下取值:

– 0 或 IPC_PRIVATE。当 key 的取值为 IPC_PRIVATE,则函数 shmget() 创建一块新的共享内存;如果 key 的取值为 0,而参数 shmflg 中设置了 IPC_PRIVATE 这个标志,则同样将创建一块新的共享内存。

– 大于 0 的 32 位整数:视参数 shmflg 来确定操作。

• size:要创建共享内存的大小,所有的内存分配操作都是以页为单位的,所以即使只申请只有一个字节的内存,内存也会分配整整一页。

• shmflg:表示创建的共享内存的模式标志参数,在真正使用时需要与 IPC 对象存取权限 mode

(如 0600)进行“|”运算来确定共享内存的存取权限。msgflg 有多种情况:

– IPC_CREAT:如果内核中不存在关键字与 key 相等的共享内存,则新建一个共享内存;如果存在这样的共享内存,返回此共享内存的标识符。

– IPC_EXCL:如果内核中不存在键值与 key 相等的共享内存,则新建一个共享内存;如果存在这样的共享内存则报错。

– SHM_HUGETLB:使用“大页面”来分配共享内存,所谓的“大页面”指的是内核为了提高程序性能,对内存实行分页管理时,采用比默认尺寸(4KB)更大的分页,以减少缺页中断。Linux 内核支持以 2MB 作为物理页面分页的基本单位。

– SHM_NORESERVE:不在交换分区中为这块共享内存保留空间。

• 返回值:shmget() 函数的返回值是共享内存的 ID。

shmat函数

如果一个进程想要访问这个共享内存,那么需要将其映射到进程的虚拟空间中,然后再去访问它,那么系统提供的 shmat() 函数就是把共享内存区对象映射到调用进程的地址空间。函数原型如下:

void *shmat(int shmid, const void *shmaddr, int shmflg);

参数说明:

• shmid:共享内存 ID,通常是由 shmget() 函数返回的。

• shmaddr:如果不为 NULL,则系统会根据 shmaddr 来选择一个合适的内存区域,如果为NULL,则系统会自动选择一个合适的虚拟内存空间地址去映射共享内存。

• shmflg:操作共享内存的方式:

  0 : 可读可写

– SHM_RDONLY:以只读方式映射共享内存。

– SHM_REMAP:重新映射,此时 shmaddr 不能为 NULL。

– NULLSHM:自动选择比 shmaddr 小的最大页对齐地址。

shmat() 函数调用成功后返回共享内存的起始地址,这样子我们就能操作这个共享内存了。

shmdt函数

  shmdt() 函数与 shmat() 函数相反,是用来解除进程与共享内存之间的映射的,在解除映射后,该进程不能再访问这个共享内存。函数原型:

int shmdt(const void *shmaddr);

参数说明:

• shmaddr:映射的共享内存的起始地址。

shmdt() 函数调用成功返回 0,如果出错则返回-1,并且将错误原因存于 error 中。

虽然 shmdt() 函数很简单,但是还是有注意要点的:该函数并不删除所指定的共享内存区,而只是将先前用 shmat() 函数映射好的共享内存脱离当前进程,共享内存还是存在于物理内存中。

shmctl函数

内核提供了 shmctl() 用于获取或者设置共享内存的相关属性。函数原型

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

• shmid:共享内存标识符。

• cmd:函数功能的控制命令,其取值如下:

– IPC_STAT:获取属性信息,放置到 buf 中。

– IPC_SET:设置属性信息为 buf 指向的内容。

– IPC_RMID:删除这该共享内存。

– IPC_INFO:获得关于共享内存的系统限制值信息。

– SHM_INFO:获得系统为共享内存消耗的资源信息。

– SHM_STAT:与 IPC_STAT 具有相同的功能,但 shmid 为该 SHM 在内核中记录所有SHM 信息的数组的下标,因此通过迭代所有的下标可以获得系统中所有 SHM 的相关信息。

– SHM_LOCK:禁止系统将该 SHM 交换至 swap 分区。

– SHM_UNLOCK:允许系统将该 SHM 交换至 swap 分。

• buf:共享内存属性信息结构体指针,设置或者获取信息都通过该结构体,shmid_ds 结构如

下:

struct shmid_ds {
struct ipc_perm shm_perm; /* 所有权和权限 */
size_t shm_segsz; /* 共享内存尺寸(字节) */
time_t shm_atime; /* 最后一次映射时间 */
time_t shm_dtime; /* 最后一个解除映射时间 */
time_t shm_ctime; /* 最后一次状态修改时间 */
pid_t shm_cpid; /* 创建者 PID */
pid_t shm_lpid; /* 后一次映射或解除映射者 PID */
shmatt_t shm_nattch; /* 映射该 SHM 的进程个数 */
...
};

其中权限信息结构体如下:

struct ipc_perm {
key_t __key; /* 该共享内存的键值 key */
uid_t uid; /* 所有者的有效 UID */
gid_t gid; /* 所有者的有效 GID */
uid_t cuid; /* 创建者的有效 UID */
gid_t cgid; /* 创建者的有效 GID */
unsigned short mode;    /* 读写权限 + SHM_DEST + SHM_LOCKED 标记 */
unsigned short __seq;        /* 序列号 */
};

使用共享内存的一般步骤是:

  1. 创建或获取共享内存 ID。

  2. 将共享内存映射至本进程虚拟内存空间的某个区域。

  3. 当不再使用时,解除映射关系。

  4. 当没有进程再需要这块共享内存时,删除它。

三.进程调度

1.进程分类

I/O消耗型进程:

大部分时间用于提交IO请求或等待IO请求,因此这样的进程常处于可运行状态,但都是运行短短的一会,因为它在等待IO请求时会阻塞。

CPU消耗信进程:

把大部分时间用于执行代码。除非被抢占,否则它们一直在运行。

此外,进程可以按照其调度需求和优先级的不同分为不同的类别:

普通进程:

又称为分时进程,这类进程在Linux系统中遵循默认的分时调度策略,如CFS(Completely Fair Scheduler)。它们按照各自权重(nice值)和虚拟运行时间(vruntime)来获取CPU时间片。nice值可以在[-20, 19]范围内调整,数值越小,优先级越高,但总体来说,普通进程之间是公平共享CPU资源的。

实时进程:

实时进程在满足特定条件的情况下需要得到及时响应,具有更高的优先级。Linux内核提供两种实时调度策略:SCHED_FIFO(先进先出)和SCHED_RR(轮转调度)。

SCHED_FIFO:实时进程中,优先级高的进程总是优先执行,一旦开始运行,除非进程主动放弃CPU(如阻塞等待I/O或睡眠),否则不会被优先级相同或更低的其他进程抢占。

SCHED_RR:同样是实时进程,但它在用完时间片后会重新加入队列等待下一次调度,这样可以保证在相同优先级的实时进程中实现时间片轮转。

期限进程:

在一些文献和系统中,也可能提到限期进程这一概念,它指的是那些具有严格截止时间要求的任务,必须在规定时间内完成。在Linux内核的标准调度器中并没有直接的限期调度策略,但在实时扩展(如PREEMPT_RT补丁集)的支持下,可以通过特殊的实时调度策略或者其他方法模拟实现这种功能。实际应用中,这种类型的进程通常归入实时进程范畴,通过设定合适的实时优先级并配合调度算法确保其能够在截止时间前完成计算。

2.优先级相关代码

在task_struct结构体中,4个优先级相关成员如下:

prio: 这个字段代表进程的动态优先级,它是根据进程的行为和系统负载动态调整的。

static_prio: 静态优先级,也称为nice值,在Linux中范围是-20至19,数值越小表示优先级越高。静态优先级可以通过nice值或者用户权限改变,但不会像动态优先级那样频繁变化

normal_prio:此字段在某些Linux调度器实现中可能用来表示经过nice值调整后的正常优先级,它结合了静态优先级和可能的额外优先级调整因素。

rt_priority:实时优先级,仅适用于实时调度策略(如SCHED_FIFO或SCHED_RR)。实时进程有固定的优先级分配,rt_priority值越大,表示进程的实时优先级越高,抢占其他进程的可能性也就越大。实时进程一般不受nice值的影响,其优先级高于普通进程。在实时调度策略下,rt_priority用于确定进程在实时进程队列中的相对位置。

优先级在不同进程中的分配:

  • 限期进程的优先级是-1;

  • 实时进程的优先级1-99,优先级数值最大,表示优先级越高;

  • 普通进程的静态优先级为: 100-139,优先级数值越小,表示优先级越高,可通过修改nice值改变普通进程的优先级,优先级等于120加 上nice值;

限期进程的优先级比实时进程要高,实时进程的优先级比普通进程要高

3.调度算法

Linux调度器是以模块方式提供的,这样做的目的是允许不同类型的进程可以有针对性地选择调度算法。

CFS算法

CFS(完全公平调度算法,一个针对普通进程的调度类):没有直接分配时间到进程,将处理器的使用比划分给进程。CFS的做法是允许每个进程运行一段时间、循环轮转、选择运行最少的进程作为下一个运行进程。

CFS 调度器没有时间片的概念,CFS 的理念就是让每个进程拥有相同的使用 CPU 的时间。比如有 n 个可运行的进程,那么每个进程将能获取的处理时间为 1/n。

在 CFS 调度器中引用权重来代表进程的优先级。各个进程按照权重的比例来分配使用 CPU 的时间。比如2个进程 A 和 B, A 的权重为 100, B 的权重为200,那么 A 获得的 CPU 的时间为 100/(100+200) = 33%, B 进程获得的CPU 的时间为 200/(100+200) = 67%。

在引入权重之后,在一个调度周期中分配给进程的运行时间计算公式如下:

实际运行时间 = 调度周期 * 进程权重 / 所有进程权重之和

可以看到,权重越大,分到的运行时间越多。

调度周期:在某个时间长度可以保证运行队列中的每个进程至少运行一次,我们把这个时间长度称为调度周期。也称为调度延迟。

调度最小粒度:为了防止进程切换太频繁,进程被调度后应该至少运行一小段时间,我们把这个时间长度称为调度最小粒度。调度周期的默认值是20毫秒,调度最小粒度的默认值是1毫秒。

调度粒度的设置, 是为了防止这么一个情况: 新进程的vruntime值只比当前进程的vruntime小一点点, 如果此时发生重新调度,则新进程只运行一点点时间后,其vruntime值就会大于前面被抢占的进程的vruntime值, 这样又会发生抢占,所以这样的情况下,系统会发生频繁的切换。故, 只有当新进程的vruntime值比当前进程的vruntime值小于调度粒度之外,才发生抢占。

如果运行队列中的进程数量太多,导致把调度周期 sysctl_sched_latency 平分给进程时的时间片小于调度最小粒度,那么调度周期取 “调度最小粒度 × 进程数量”。

CFS 调度器中使用 nice 值(取值范围为[-20 ~ 19])作为进程获取处理器运行比的权重:nice 值越高(优先级越低)的进程获得的 CPU使用的权重越低。

nice 值与 CFS 调度器中的权重关系:

nice值共有40个,与权重之间,每一个nice值相差10%左右。
const int sched_prio_to_weight[40] = {
 /* -20 */     88761,     71755,     56483,     46273,     36291,
 /* -15 */     29154,     23254,     18705,     14949,     11916,
 /* -10 */      9548,      7620,      6100,      4904,      3906,
 /*  -5 */      3121,      2501,      1991,      1586,      1277,
 /*   0 */      1024,       820,       655,       526,       423,
 /*   5 */       335,       272,       215,       172,       137,
 /*  10 */       110,        87,        70,        56,        45,
 /*  15 */        36,        29,        23,        18,        15,
}; 

从 nice 和权重的对应值可知,nice 值为 0 的权重为 1024(默认权重), nice 值为1的权重为 820,nice 值为 15 的权重值为 36,nice 值为19的权重值为 15。

例如:假设调度周期为12ms,2个相同nice的进程其权重也相同,那么2个进程各自的运行时间为 6ms。

假设进程 A 和 B 的 nice 值分别为0、1,那么权重也就分别为 1024、820。因此,A 实际运行时间为 12 * 1024/(1024+820)= 6.66ms ,B 实际运行时间为 12 * 820/(1024+820)= 5.34ms。从结果来看,2个进程运行时间时不一样的。由于A的权重高,优先级大,会出现 A 一直被调度,而 B 最后被调度,这就失去了公平性,所以 CFS 的存在就是为了解决 这种不公平性。

因此为了让每个进程完全公平调度,因此就引入了一个 vruntime (虚拟运行时间,virtual runtime)的概念, 每个调度实体都有一个 vruntime,该vruntime 根据调度实体的调度而不停的累加,CFS 根据 vruntime 的大小来选择调度实体。

调度实体的结构如下:

struct sched_entity {
        struct load_weight                load;
        struct rb_node                run_node;
        unsigned int                on_rq;
        u64                        sum_exec_runtime;
        u64                        vruntime;
}; 

load:权重信息,在计算虚拟时间的时候会用到inv_weight成员。

run_node:CFS调度器的每个就绪队列维护了一颗红黑树,上面挂满了就绪等待执行的task,run_node就是挂载点。

on_rq:调度实体se加入就绪队列后,on_rq置1。从就绪队列删除后,on_rq置0。

sum_exec_runtime:调度实体已经运行实际时间总合。

vruntime:调度实体已经运行的虚拟时间总合。

虚拟运行时间,在时间中断或者任务状态发生改变时会更新其会不停的增长,增长速度与load权重成反比,load越高,增长速度越慢,就越可能处于红黑树最左边被调度。每次时钟中断都会修改其值。注意其值为单调递增,在每个调度器的时钟中断是当前进程的虚拟运行时间都会累加。单纯的说就是进程们都在比谁的vruntime最小,最小的将被调度。

虚拟时间和实际时间的关系如下

虚拟运行时间 = 实际运行时间 *(NICE_0_LOAD / 进程权重)
#其中,NICE_0_LOAD 是 nice为0时的权重(默认),也即是 1024。
#也就是说,nice 值为0的进程实际运行时间和虚拟运行时间相同。

虚拟运行时间 = 实际运行时间 *(NICE_0_LOAD / 进程权重) #其中,NICE_0_LOAD 是 nice为0时的权重(默认),也即是 1024。 #也就是说,nice 值为0的进程实际运行时间和虚拟运行时间相同。

虚拟运行时间一方面跟进程运行时间有关,另一方面跟进程优先级有关。进程权重越大, 运行同样的实际时间, vruntime 增长的越慢。一个进程在一个调度周期内的虚拟运行时间大小为:

vruntime = 进程在一个调度周期内的实际运行时间 * 1024 / 进程权重
         = (调度周期 * 进程权重 / 所有进程总权重) * 1024 / 进程权重
         = 调度周期 * 1024 / 所有进程总权重。

可以看到, 一个进程在一个调度周期内的 vruntime 值大小是不和该进程自己的权重相关的, 所以所有进程的 vruntime 值大小都是一样的。

接着上述的例子,通过虚拟运行时间公式可得:

A 虚拟运行时间为 6.66 * (1024/1024) = 6.66ms,

B 虚拟运行时间为 5.34* (1024/820) = 6.66ms,

在一个调度周期过程中,各个调度实体的 vruntime 都是累加的过程,保证了在一个调度周期结束后,每个调度实体的 vruntime 值大小都是一样的。由于权重越高,应该优先的得到运行,因此 CFS 采用虚拟运行时间越小,越先调度。

当权重越高的进程随着调度的次数多,其 vruntime 的累加也就越多。当其 vruntime 的累加大于其他低优先级进程的 vruntime 时,低优先级的进程得以调度。这就保证了每个进程都可以调度而不会出现高优先级的一直得到调度,而优先级低的进程得不到调度而产生饥饿。一言以蔽之:在CFS中,不管权重高低,根据 vruntime 大小比较,大家都轮着使用 CPU

当然,根据一个调度周期中分配给进程的实际运行时间计算公式可知,在一个调度周期内,虽然大家都轮着使用 CPU,但是实际运行时间的多少和权重也是有关的,权重越高,总的实际运行的时间也就越多。在一个调度周期结束后,各个调度实体的 vruntime 最终还是相等的。

CFS 中的就绪队列是一棵以 vruntime 为键值的红黑树,虚拟时间越小的进程越靠近整个红黑树的最左端。因此,调度器每次选择位于红黑树最左端的那个进程,该进程的 vruntime 最小,也就最应该优先调度。

实际获取最左叶子节点时并不会遍历树,而是 vruntime 最小的节点已经缓存在了 rb_leftmost 字段中了,因此 CFS 很快可以获取 vruntime 最小的节点。

算法实现:

Linux通过struct task_struct结构体描述每一个进程。但是调度类管理和调度的单位是调度实体,并不是task_struct。所以,我们在struct task_struct结构体中可以找到以下不同调度类的调度实体。

struct task_struct {
        struct sched_entity                   se;
        struct sched_rt_entity                rt;
        struct sched_dl_entity                dl;
    /* ... */
} 
 
se、rt、dl分别对应CFS调度器、RT调度器、Deadline调度器的调度实体。

struct sched_entity结构体描述调度实体,包括struct load_weight用来记录权重信息。除此以外我们一直关心的时间信息,肯定也要一起记录。struct sched_entity结构体简化后如下:

更新进程虚拟运行时间:

进程选择,挑选下一个任务

向树中加入进程,

实时调度算法(待补充)

进程相关指令

Ps 查看进程

ps命令最常用的两个选项ps aux、ps axjf 字母选项的含义:

a--显示1个终端所有进程。

u--显示进程的归属用户及内存使用情况。

x--显示没有关联控制终端的进程。

j--显示进程归属的进程组ID、会话ID、父进程ID。

f--以ASCII形式显示出进程的层次关系。

Kill 杀死进程

当执行“kill”命令,实际上发送了一个信号给系统,告诉它去终结不正常的应用。总共有60个可以使用的信号,但是基本上只需要知道SIGTERM(15)(正常杀死信号)和SIGKILL(9)(强制杀死信号)

你可以用这个命令看到所有信号的列表:

kill -l

如想要强制结束进程,则需要发送9号信号给系统,应该是这样的:kill -9 pid号

pkill 可以用根据进程名称来杀死进程

nice改变进程优先级

普通用户只能在0~19之间调整应用程序的优先权值,超级用户有权在-20~19之间调整。

基本语法:

nice [OPTION] [COMMAND [ARG]...]

其他指令

Tar 压缩/解压

在vim编辑器中显示行数

进程与线程区别?

进程是正在运行的程序,是资源分配的最小单位。

线程是进程的一个执行流,是CPU调度和执行的基本单位,它是比进程更小的能独立运行的基本单位。一个进程由几个线程组成。

不同的进程之间拥有独立的地址空间,互不影响。线程与同属一个进程的其他的线程共享进程所拥有的资源。

线程是存在进程的内部,一个进程中可以有多个线程,一个线程只能存在一个进程中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

嵌入式小李

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值