Linux线程调度策略

内容

  1. 先来先服务(FIFO)调度策略
  2. 时间片轮转调度策略
  3. 分时调度策略

Linux内核线程的三种调度策略

函数pthread_attr_setschedpolicypthread_attr_getschedpolicy分别用来设置、获取线程的调度策略。

1
2
3
4
5
6
7
int pthread_attr_setschedpolicy(pthread_attr_t *, int policy);
int pthread_attr_getschedpolicy(const pthread_attr_t *, int policy);
参数:
attr - 线程属性变量
policy - 调度策略
返回值
成功返回0,失败返回-1
  1. SCHED_OTHER:分时调度策略,非实时。不支持优先级。
  2. SCHED_FIFO:先到先服务,实时。一旦占用cpu则一直运行。直到有更高优先级任务或主动放弃cpu。
  3. SCHED_RR:时间片轮转,实时。当进程的时间片用完,重新分配时间片长度,置于就绪队列尾。可以看作是FIFO的延伸。

先到先服务策略

  1. 创建进程时指定采用FIFO,并设置实时优先级rt_priority(1-99)。
  2. 如果没有等待资源,则将该任务加入到就绪队列中。
  3. 调度程序遍历就绪队列,根据实时优先级计算调度权值(1000+rt_priority),选择权值最高的任务使用cpu,该FIFO任务将一直占有cpu直到有优先级更高的任务就绪(即使优先级相同也不行)或者主动放弃(等待资源)
  4. 调度程序发现有优先级更高的任务到达(高优先级任务可能被中断或定时器任务唤醒,再或被当前运行的任务唤醒等等),则调度程序立即在当前任务堆栈中保存当前cpu寄存器的所有数据,重新从高优先级任务的堆栈中加载寄存器数据到cpu,此时高优先级的任务开始运行。重复第3步。
  5. 如果当前任务因等待资源而主动放弃cpu使用权,则该任务将从就绪队列中删除,加入等待队列,此时重复第3步。

时间片轮转策略

所有任务都采用RR调度策略时:

  1. 创建任务时指定调度参数为RR,并设置任务的实时优先级nice值(nice值将会转换为该任务的时间片的长度)。

    可见,相比于FIFO,时间片轮转法多了一个nice值,用于计算时间片长度。

  2. 如果没有等待资源,则将该任务加入到就绪队列中。

  3. 调度程序遍历就绪队列,根据实时优先级计算调度权值(1000+rt_priority),选择权值最高的任务使用cpu。

    相比于FIFO,FIFO会一直占用CPU,直到更高优先级线程到达。

  4. 如果就绪队列中的RR任务时间片为0,则会根据nice值设置该任务的时间片,同时将该任务放入就绪队列的末尾。重复步骤3。

  5. 当前任务由于等待资源而主动退出cpu,则其加入等待队列中。重复步骤3。

分时调度策略

所有任务都采用Linux分时调度策略时:

  1. 创建任务指定采用分时调度策略,并指定nice值(-20~19, 可以认为值越小优先级越高)。

    相比于FIFO,没有实时优先级数值属性(1~99)

  2. 根据每个任务的nice值确定在cpu上的执行时间(counter)。

  3. 如果没有等待资源,则将该任务加入到就绪队列中。

  4. 调度程序遍历就绪队列中的任务,通过对每个任务动态优先级的计算权值(counter+20-nice)结果,选择计算结果最大的一个去运行,当这个时间片用完后(counter减至0)或者主动放弃cpu时,该任务将被放在就绪队列末尾(时间片用完)或等待队列(因等待资源而放弃cpu)中。

  5. 此时调度程序重复上面计算过程,转到第4步。

  6. 当调度程序发现所有就绪任务计算所得的权值都不大于0时,重复第2步。

区别

系统中既有分时调度,又有时间片轮转调度和先进先出调度:

  1. RR调度和FIFO调度的进程属于实时进程,以分时调度的进程是非实时进程。
  2. 当实时进程准备就绪后,如果当前cpu正在运行非实时进程,则实时进程立即抢占非实时进程。
  3. RR进程和FIFO进程都采用实时优先级做为调度的权值标准,RR是FIFO的一个延伸。
  4. FIFO时,如果两个进程的优先级一样,则这两个优先级一样的进程具体执行哪一个是由其在队列中的未知决定的,这样导致一些不公正性(优先级是一样的,为什么要让你一直运行?),如果将两个优先级一样的任务的调度策略都设为RR,则保证了这两个任务可以循环执行,保证了公平。

Linux_IO复用_epoll

内容

  1. 基本概念
  2. epoll

epoll

epoll是Linux特有的I/O复用函数。

epoll的使用实际上不是单独的API,而是有一组函数来完成。三个函数。

API

1
2
3
4
#include<sys/epoll.h>
int epoll_create(int size); //创建内核时间表,用于存放描述符和事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
  1. epoll_create():创建内核事件表,存放描述符和事件,红黑树。
  2. epoll_ctl():添加、移除描述符,每个描述符只需要添加一次。
  3. epoll_wait():获取已就绪的描述符,复杂度O(1)

内核怎么样实现O(1)?:注册回调函数的方式

epoll_create

epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无须像select和poll那样每次调用都要重复传描述符集或事件集。

但epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。

这个文件描述符就是使用epoll_create函数来创建。

1
int epoll_create(int size);

只有一个参数size,但是实际上这个参数并不起作用,只是给内核一个提示,告诉他事件表大概需要多大。

函数返回值为对应这个事件表的一个文件描述符。其他所有epoll系统调用的第一个参数将使用该返回值,以指定要访问的内核事件表。

epoll_ctl

1
2
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// epoll_ctl成功时返回0,失败则返回-1并设置errno。
  1. 参数epfdepoll_create返回值。

  2. 参数op指定操作类型。操作类型有如下3种:

    1. EPOLL_CTL_ADD,往事件表中注册fd上的事件;
    2. EPOLL_CTL_MOD,修改fd上的注册事件;
    3. EPOLL_CTL_DEL,删除fd上的注册事件;
  3. 参数fd是要操作的文件描述符

  4. 参数event指定事件,它是epoll_event结构指针类型

    1. epoll_event的定义如下

      1
      2
      3
      4
      5
      struct epoll_event
      {
      __uint32_t events; /* epoll事件, 和poll类似,前加E*/
      epoll_data_t data; /* 用户数据 */
      }
      1. 结构体中成员events描述事件类型。epoll支持的事件类型与poll基本相同。表示epoll事件类型的宏是在poll对应的宏前加上"E",比如数据可读事件是EPOLLIN。但epoll有两个额外的事件类型——EPOLLETEPOLLONESHOT,这俩事件往往对应epoll的高效运作模式。

      2. data成员用于存储用户数据。其类型epoll_data_t的定义如下:

        1
        2
        3
        4
        5
        6
        7
        typedef union epoll_data
        {
        void * ptr;
        int fd;
        uint32_t u32;
        uint64_t u64;
        }epoll_data_t;

        epoll_data_t是一个联合体,其4个成员中:

        1. 使用最多的是fd,它指定事件所从属的目标文件描述符。
        2. 成员ptr可用来指定与fd相关的用户数据。但由于epoll_data_t是一个联合体,不可同时使用ptrfd,因此,如果要将文件描述符和用户数据关联起来以实现快速的数据访问的话,只能使用其他手段。比如放弃使用fd成员,而在ptr指向的用户数据中包含fd

epoll_wait

在一段超时时间内等待一组文件描述符上的事件。

1
2
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
// 成功返回就绪的文件描述符的个数,失败则返回-1并设置errno

epoll_wait函数如果检测到事件,就将所有就绪的事件从内核事件表(由epfd参数指出)中复制到它的第二个参数events指向的数组中。这个数组只用于用户接收内核检测到的就绪事件,而不像select/poll的数组参数那样拷贝来拷贝去。极大地提高了应用程序索引就绪文件描述符的效率。

参数maxevents指定最多监听多少个事件,必须大于0。

参数timeout含义与poll接口的timeout参数相同,都是指定超时值,单位是毫秒。当设置为-1时,poll调用讲永远阻塞直到某个事件发生;当设置为0poll调用将立即返回。

与poll的代码差异

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int ret = poll(fds, MAX_EVENT_NUMBER, -1);
for(int i = 0; i<MAX_EVENT_NUMBER, ++i)
{
if(fds[i].revents & POLLIN)
{
int sockfd = fds[i].fd;
/* 处理sockfd */
}
}
//以上为poll
-----------------------------------------------------
//以下为epoll
int ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
for(int i = 0; i < ret; ++i)
{
int sockfd = events[i].data.fd;
/* sockfd肯定就绪,直接处理 */
}

内核事件表

epoll的最大优势在于处理描述符特别多的情况,相比轮询方式:

  1. 如果用传统的select、poll单纯的顺序轮询方法监测就绪的描述符,那么性能会很低下的。
  2. 而且虽然select、poll能顺利查出有几个描述符上有事件产生,但是是哪个描述符?并没有告诉我们,所以又得浪费一轮时间来查找产生时间是哪个描述符。

数据结构

本质上,是一棵红黑树。

思路的转变

相较于select/poll,我们所关心的事件(描述符)表,直接创建于内核态。

LT/ET模式

epoll对文件描述符的操作有两种模式:LT (Level Trigger, 电平触发)模式和ET (Edge Trigger, 边沿触发)模式。

  • LT:有事件就绪后,用户不用立即处理;用户如果没有处理完,还会继续提醒
  • ET:有事件就绪,只提醒用户一次。下次就没了。

LT

LT模式是默认的工作模式,这种模式下epoll相当于一个效率较高的poll。

对于采用LT工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用epoll_wait时,epoll_wait还会再次向应用程序通告此事件,直到该事件被处理。

ET

当往epoll内核事件表中注册一个文件描述符上的EPOLLET事件时,epoll将以ET模式来操作该文件描述符。ET模式是epoll的高效工作模式。

而对于采用ET工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait调用将不再向应用程序通知这一事件。可见,ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,因此效率要比LT模式高。

LT - 代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#include<string.h>
#include<assert.h>
#include<sys/socket.h>
#incldue<netinet/in.h>
#include<arpa/inet.h>

#include<sys/epoll.h>

#define MAX 10
int socket_init();
int epoll_add(int epfd, int fd)
{
struct epoll_event ev;
ev.data.fd = fd;
ev.events = EPOLLIN;//ev.events = EPOLLIN | EPOLLET; 这样可工作在ET模式

if(epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) == -1)
{
perror("epoll ctl add err\n");
}
}
void epoll_del(int epfd, int fd)
{
if(epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL) == -1)
{
perror("epoll ctl del err\n");
}
}
int main()
{
int sockfd = socket_init();
assert(sockfd != -1);
int epfd = epoll_create(MAX);
assert(epfd != -1);

epoll_add(epfd, sockfd);//将监听套接字添加到内核事件表
struct epoll_event evs[MAX];
while(1)
{
int n = epoll_wait(epfd, evs, MAX, 5000); //5000ms
if(n == -1)
{
perror("epoll wait err\n");
}
else if(n == 0)
{
printf("time out\n");
}
else
{
for(int i = 0; i < n; ++i)//每个事件均为就绪状态
{
int fd = evs[i].data.fd;
if(evs[i].events & EPOLLIN)
{
if(fd == sockfd)//监听套接字
{
struct sockaddr_in caddr;
int len = sizeof(caddr);
int c = accept(sockfd, (struct sockaddr*)&caddr, &len);
if(c < 0)
{
continue;
}
printf("accept c = %d\n", c);

epoll_add(epfd, c);
}
else //连接套接字
{
char buff[128] = {0};
int num = recv(fd, buff, 127, 0);
if(num <= 0)//对方关闭 或异常,需要移除
{
//注意不要先close,因为epoll_del需要用到fd描述符。
epoll_del(epfd, fd);
close(fd);
printf("client close\n");
}
else
{
printf("recv(%d)=%s\n", fd, buff);
send(fd, "ok", 2, 0);
}
}
}
}
}
}
}

ET - 代码示例

主要解决的问题:怎么一次性把描述符上的数据接收完?

  1. 描述符设置成非阻塞

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #include<fcntl.h>
    int fcntl(int fd, int cmd, ... /* arg */);
    /* fcntl()可以采用可选的第三个参数。是否需要这个参数由 cmd 决定。
    所需的参数类型在每个 cmd 名称后面的括号中指示(在大多数情况下,所需的类型是 int,我们使用名称 arg 来标识参数),如果不需要参数,则指定 void。
    只有特定的 Linux 内核版本才支持某些 cmd 操作。检查主机内核是否支持特定操作的首选方法是使用所需的 cmd 值调用 fcntl(),然后测试调用是否以 EINVAL 失败,表明内核无法识别该值。*/
    /*
    For a successful call, the return value depends on the operation:
    F_DUPFD The new file descriptor.
    F_GETFD Value of file descriptor flags.
    F_GETFL Value of file status flags.
    F_GETSIG Value of signal sent when read or write becomes possible, or zero for traditional SIGIO behavior.

    All other commands : Zero.
    On error, -1 is returned, and errno is set appropriately.
    */
    可选的cmd操作(其中之二):
    F_GETFL(void)//get file access mode and the file status flags
    F_SETFL(int)//set file status flags to the value by arg. File access mode and file creation flags in arg are ignored. On Linux, this command can only change the O_APPEND, O_ASYNC, O_DIRECT, O_NOATIME, O_NONBLOCK flags. It's not possible to change the O_DSYNC, O_SYNC flags.
  2. 循环处理(读取)
    需要判断recv返回值为-1时的错误类型(没数据时,非阻塞情况下立即返回-1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    返回值
    如果发生错误,则返回-1,设置errno以指示错误。
    当流套接字对等点执行了有序关闭时,返回值将为0(传统的“文件结束”返回)。
    成功则返回接收到的字节数;

    各种域(例如,UNIX 和 Internet 域)中的数据报套接字允许零长度数据报。当接收到这样的数据报时,返回值为 0。
    如果从流套接字接收的请求字节数为 0,则也可能返回值 0。

    错误
    这些是套接字层产生的一些标准错误。底层协议模块可能会产生和返回额外的错误;查看他们的手册页。

    EAGAIN 或 EWOULDBLOCK
    套接字被标记为非阻塞并且接收操作将阻塞,或者已设置接收超时并且在接收数据之前超时已过期。POSIX.1允许在这种情况下返回任一错误,并且不要求这些常量具有相同的值,因此可移植应用程序应检查这两种可能性。

    EBADF
    参数 sockfd 是无效的文件描述符。
    ECONNREFUSED
    远程主机拒绝允许网络连接(通常是因为它没有运行请求的服务)。
    EFAULT
    接收缓冲区指针指向进程地址空间之外。
    EINTR
    在任何数据可用之前,接收被信号传递中断;见signal(7)。
    EINVAL
    传递了无效的参数。
    ENOMEM
    无法为 recvmsg() 分配内存。
    ENOTCONN
    套接字与面向连接的协议相关联,并且尚未连接(请参阅connect(2)和 accept(2))
    ENOTSOCK
    文件描述符 sockfd 不引用套接字。
  • 设置为非阻塞
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void setnonblock(int fd)
{
int oldfilestatusflags = fcntl(fd, F_GETFL);
int newfilestatusflags = oldfilestatusflags | O_NONBLOCK;
if(fcntl(fd, F_SETFL, newfilestatusflags) == -1)
{
perror("fcntl setfl err\n");
}
}
void epoll_add(int epfd, int fd)
{
struct epoll_event ev;
ev.data.fd = fd;
ev.events = EPOLLIN | EPOLLET;
if(epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) == -1)
{
perror("epoll ctl add err\n");
}
setnonblock(fd); //这个操作放在前后不影响,与epoll_ctl独立。
}
  • 循环处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include<errno.h>
else
{
while(1)
{
char buff[128] = {0};
int num = recv(fd, buff, 1, 0); //1次读1个字节
if(num == -1)
{
if(errno == EAGAIN || errno == EWOULDBLOCK)//没有数据了
{
send(fd, "ok", 2, 0);
}
else
{
perror("recv err");
}
break;
}
else if(num == 0)//对方关闭了
{
epoll_del(epfd, fd);
close(fd);
printf("client close\n");
break;
}
else //这一次的recv有数据
{
printf("buff=%s\n", buff);
}
}
}