LucienXian's Blog


  • 首页

  • 归档

  • 标签

Linux内核学习——内核同步介绍

发表于 2018-07-22

内核同步介绍

临界区和竞争条件

临界区:访问和操作共享数据的代码段。

对于单个变量的操作,处理器提供了指令来原子地读变量,修改变量和返回变量。

加锁

造成并发执行的原因

用户空间之所以需要同步,是因为用户程序会被调度程序抢占和重新调度。内核造成并发执行的原因:

  • 中断
  • 软中断和tasklet
  • 内核抢占
  • 睡眠以及用户空间的同步
  • 对称多处理

死锁

最简单的死锁例子是自死锁:一个执行线程试图去获得一个自己已经持有的锁。一些简单的规则可以帮助避免死锁:

  • 按顺序加锁;
  • 防止发送饥饿;
  • 不重复请求同一个锁;

争用和扩展性

锁的争用是指当锁正被使用时,有其它线程试图获得该锁。

加锁的粒度用来描述加锁保护的数据规模,当锁争用很严重时,加锁太粗会降低可扩展性;当争用不明显时,加锁过细又会加大系统开销。

Linux内核学习——下半部与推后执行的工作

发表于 2018-07-21

下半部与推后执行的工作

中断处理程序异步执行,需要快速地执行硬件与操作系统的操作,避免其它中断停止太久,因此中断处理程序不能阻塞。

下半部

上半部只能通过中断处理程序实现,而下半部则提供了多种实现机制——软中断、tasklet、工作队列。

内核定时器,这个机制用来将工作推后执行,推后到某个明确的时间段之后执行。

软中断

软中断是一组静态定义的下半部接口,有32个。

1
2
3
struct softirq_action {
void (*action)(struct softirq_action *);
};
1
static struct softirq_action softirq_vec[NR_SOFTIRQS];
  1. 软中断处理程序
1
2
3
4
void sofrirq_handler(struct softirq_action *)
{
my_softirq->action(my_softirq);
}

之所以传递一个指针而不是一个数值,是因为防止后面加上新的域时,无需对处理程序进行改动;

  1. 执行软中断

一个注册的软中断会在中断处理程序返回前标记了之后才能被执行,会在标记之后一定时间内执行。

在中断处理程序中触发软中断是比较常见的。软中断会在do_softirq()中遍历所有软中断,进行调用。

tasklet

tasklet是使用软中断实现的一种下半部机制,接口更加简单。

tasklet的实现

tasklet有两种软中断代表:HI_SOFTIRQ和TASKLET_SOFTIRQ。

  1. tasklet结构体
1
2
3
4
5
6
7
struct tasklet_struct{
struct tasklet_struct *next; //链表中下一个tasklet_struct
unsigned long state; //0, TASKLET_STATE_SCHED,TASKLET_STATE_RUN
atomic_t count; //为0时才被激活
void (*func)(unsigned long);//tasklet处理函数
unsigned long data; //给tasklet
};
  1. 调度tasklet

已经调度的tasklet相当于被触发的软中断,会被放在两个数据结构里——tasklet_vec(普通tasklet)和tasklet_hi_vec(高优先级的tasklet)。

tasklet由tasklet_schedule()和tasklet_hi_schedule()进行调度,它接受一个指向tasklet_struct结构的指针作为参数。

执行步骤:

  1. 检查tasklet的状态,如果是TASKLET_STATE_SCHED则直接返回;
  2. 调用_tasklet_schedule();
  3. 保持中断状态,禁止本地中断;
  4. 把需要调度的tasklet加到链表里;
  5. 唤起软中断,这样在后面调用do_softriq()时执行这个tasklet;
  6. 恢复中断到原状态返回;

工作队列

工作队列是另一种将工作推后实现的机制,它是通过将工作交由一个内核线程在进程上下文执行的方法。工作队列可以重新调度,也可以睡眠。

下半部机制的选择

  • 软中断处于中断上下文里,同类型的软中断能同时执行;
  • tasklet处于中断上下文里,同类型不能同时执行;
  • 工作队列处于进程上下文,能睡眠和唤醒;

Linux内核学习——中断和中断处理

发表于 2018-07-19

中断和中断处理

中断

中断本质上是一种电信号,由硬件设备发向处理器,处理器接受到中断之后,就会向操作系统反映该中断,让操作系统去处理新来的数据。

不同的设备对应的中断不同,因此每个中断需要通过一个中断值来进行标识。

异常也是一种同步中断,很多处理器体系结构处理异常的方式与中断类似

中断处理程序

在Linux中,中断处理程序就是普通的C函数,但要按照特定的类型进行声明,这样内核才能用这个处理程序去响应中断。同时,这个中断响应程序位于中断上下文中,这是一种原子上下文,不可被阻塞。

中断发生后,操作系统接受到信号会通知硬件设备,并且还会做一下数据操作。

上半部与下半部的对比

为了使得中断处理程序能够运行得又快,完成工作量也大,一般把中断处理程序分为两部分,上半部处理紧急的,有严格限时的工作,下半部会执行一些其它的诸如处理和操作数据的工作。

中断上下文

当中断处理程序运行时,内核处于中断上下文中。

中断上下文不能睡眠,应该尽量简洁,避免循环出现,这样才能保证中断处理程序迅速。

中断处理机制的实现

中断处理系统在Linux中是依赖于体系结构的。

设备产生中断——>电信号被传到中断控制器——>如果中断线是激活状态,则把信号传到CPU——>CPU接收到信号之后,会定义正在做的事,关闭中断系统,跳转到内核预定义的内存位置,则中断处理程序的入口点。

/proc/interrupts

在这个文件中存放着系统中与中断相关的统计信息,可以看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
          CPU0       CPU1       CPU2       CPU3       
0: 15 0 0 0 IO-APIC 2-edge timer
1: 9 9438 1199 1250 IO-APIC 1-edge i8042
8: 0 1 0 0 IO-APIC 8-edge rtc0
9: 144 26 16 13 IO-APIC 9-fasteoi acpi
12: 36 10535 1621 1496 IO-APIC 12-edge i8042
16: 353 545207 28 43 IO-APIC 16-fasteoi ehci_hcd:usb1, wlp8s0
23: 8 19 6 2 IO-APIC 23-fasteoi ehci_hcd:usb2
24: 0 0 0 0 PCI-MSI 16384-edge PCIe PME
25: 0 0 0 0 PCI-MSI 458752-edge PCIe PME
26: 0 0 0 0 PCI-MSI 466944-edge PCIe PME
27: 255 6690 14093 39701 PCI-MSI 327680-edge xhci_hcd

这里就显示了中断号,和对应的接收中断数目的计数器。

Tensorflow安装指南

发表于 2018-07-19

Tensorflow安装指南

简介

当然你可以查看

因为cs231n要使用TensorFlow完成作业,就只能硬着头皮安装了GPU版本的TensorFlow了。此次安装版本为CUDA9.0和cuDNN7.0

我的电脑显卡为:GeForce GTX 850M

硬件支持:

Distribution Kernel GCC GLIBC ICC PGI XLC CLANG
x86_64
RHEL 7.x 3.10.0 4.8.5 2.17 17.0 18.x NO 5.0.0
RHEL 6.x 2.6.32 4.4.7 2.12
CentOS 7.x 3.10.0 4.8.5 2.17
CentOS 6.x 2.6.32 4.4.7 2.12
Fedora 27 4.15.x 7.3.1 2.26
OpenSUSE Leap 42.3 4.4.120 4.8.5 2.22
SLES 12 SP3 4.4.120 4.8.5 2.22
Ubuntu 17.10 4.13 7.2.0 2.26
Ubuntu 16.04.4(**) 4.4.0 5.4.0 2.23
POWER8(***)
RHEL 7.x 3.10.0 4.8.5 2.17 NO 18.x 13.1.6 5.0.0
Ubuntu 16.04.4 4.4.0 5.4.0 2.23 NO 18.x 13.1.6 5.0.0
POWER9(****)
RHEL 7.5 IBM Power LE 4.14.0 4.8.5 2.17 NO 18.x 13.1.6 5.0.0

安装CUDA

我这里选择的是9.0

img
img

下载完之后,一次输入以下指令:

1
2
3
4
sudo dpkg -i cuda-repo-ubuntu1604-9-0-local_9.0.176-1_amd64.deb
sudo apt-key add /var/cuda-repo-<version>/7fa2af80.pub
sudo apt-get update
sudo apt-get install cuda

在下载安装过程中非常慢,因此我换了阿里云的源(不一定需要换源,如果速度够快的话):

  1. 备份原来的源
1
sudo cp /etc/apt/sources.list /etc/apt/sources.list_backup.list
  1. 更换源
1
sudo gedit /etc/apt/sources.list

复制以下的源地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
deb-src http://archive.ubuntu.com/ubuntu xenial main restricted #Added by software-properties
deb http://mirrors.aliyun.com/ubuntu/ xenial main restricted
deb-src http://mirrors.aliyun.com/ubuntu/ xenial main restricted multiverse universe #Added by software-properties
deb http://mirrors.aliyun.com/ubuntu/ xenial-updates main restricted
deb-src http://mirrors.aliyun.com/ubuntu/ xenial-updates main restricted multiverse universe #Added by software-properties
deb http://mirrors.aliyun.com/ubuntu/ xenial universe
deb http://mirrors.aliyun.com/ubuntu/ xenial-updates universe
deb http://mirrors.aliyun.com/ubuntu/ xenial multiverse
deb http://mirrors.aliyun.com/ubuntu/ xenial-updates multiverse
deb http://mirrors.aliyun.com/ubuntu/ xenial-backports main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ xenial-backports main restricted universe multiverse #Added by software-properties
deb http://archive.canonical.com/ubuntu xenial partner
deb-src http://archive.canonical.com/ubuntu xenial partner
deb http://mirrors.aliyun.com/ubuntu/ xenial-security main restricted
deb-src http://mirrors.aliyun.com/ubuntu/ xenial-security main restricted multiverse universe #Added by software-properties
deb http://mirrors.aliyun.com/ubuntu/ xenial-security universe
deb http://mirrors.aliyun.com/ubuntu/ xenial-security multiverse
  1. 更新
1
sudo apt-get update

安装CUDA之后,需要添加环境变量:

1
2
3
4
5
export PATH=/usr/local/cuda-9.0/bin${PATH:+:${PATH}}
# 64bit
export LD_LIBRARY_PATH=/usr/local/cuda-9.0/lib64${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}
# 32bit
export LD_LIBRARY_PATH=/usr/local/cuda-9.0/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}

安装cuDNN

在这里注册一下,要做一个下的问卷调查。

进入download界面

勾选I Agree To the Terms of the cuDNN Software License Agreement

我选择的是v7.0.5版本的,记得Runtime Library和Developer Library都要下载,即:

  • cuDNN v7.0.5 Runtime Library for Ubuntu16.04 (Deb)
  • cuDNN v7.0.5 Developer Library for Ubuntu16.04 (Deb)
img
img

一开始我只下载了Developer Library ,在安装的时候出现了这样的错误:

Package libcudnn7 is not installed.

两个都下载完之后,进入deb包目录,打开终端,输入:

1
sudo dpkg -i libcudnn7*.deb

输入nvidia-smi,看到:

img
img

如果显示报错,建议重启一下

安装TensorFlow

可以查看官网,选择一种安装方式。建议使用 Virtualenv 进行安装

验证安装

进入交互界面,输入以下代码:

1
2
3
4
5
# Python
import tensorflow as tf
hello = tf.constant('Hello, TensorFlow!')
sess = tf.Session()
print(sess.run(hello))

如果显示了显卡信息,就基本没有问题了,我的显示如下:

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
Python 3.5.2 (default, Nov 23 2017, 16:37:01) 
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import tensorflow as tf
/home/lucienxian/Study/cs231n/assignment2/tensorflow/lib/python3.5/site-packages/h5py/__init__.py:34: FutureWarning: Conversion of the second argument of issubdtype from `float` to `np.floating` is deprecated. In future, it will be treated as `np.float64 == np.dtype(float).type`.
from ._conv import register_converters as _register_converters
>>> hello = tf.constant('Hello,world!')
>>> sess = tf.Session()
2018-07-19 14:08:56.590394: I tensorflow/core/platform/cpu_feature_guard.cc:141] Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2 FMA
2018-07-19 14:08:56.675095: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:897] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2018-07-19 14:08:56.675799: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1392] Found device 0 with properties:
name: GeForce GTX 850M major: 5 minor: 0 memoryClockRate(GHz): 0.8625
pciBusID: 0000:01:00.0
totalMemory: 1.96GiB freeMemory: 1.73GiB
2018-07-19 14:08:56.675820: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1471] Adding visible gpu devices: 0
2018-07-19 14:09:14.060705: I tensorflow/core/common_runtime/gpu/gpu_device.cc:952] Device interconnect StreamExecutor with strength 1 edge matrix:
2018-07-19 14:09:14.060759: I tensorflow/core/common_runtime/gpu/gpu_device.cc:958] 0
2018-07-19 14:09:14.060766: I tensorflow/core/common_runtime/gpu/gpu_device.cc:971] 0: N
2018-07-19 14:09:14.060961: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1084] Created TensorFlow device (/job:localhost/replica:0/task:0/device:GPU:0 with 1489 MB memory) -> physical GPU (device: 0, name: GeForce GTX 850M, pci bus id: 0000:01:00.0, compute capability: 5.0)
>>> result = sess.run(hello)
>>> sess.close()
>>> print(result)
b'Hello,world!'
>>> exit()

Linux内核学习——数据结构

发表于 2018-07-18

内核数据结构

链表

1
2
3
4
struct list_head {
struct list_head *next;
struct list_head *prev;
};

在linux中,链表的实现是非常特别的——它不是把数据结构塞进链表里,而是将链表塞进数据结构里。这样就可以重用定义好的struct结构了。比如:

1
2
3
4
5
6
struct fox{
unsigned long tail_length;
unsigned long weight;
bool is_fantastic;
struct list_head list;
};

但这样怎么找到结构体foo的其它变量呢,方法很简单,由于结构体的偏移量在编译时已经确定下来了,使用宏定义container_of()我们就可以找到父结构的任何变量。

1
2
3
4
#define container_of(ptr, type, member) ({				\
const typeof(((type *)0)->member) * __mptr = (ptr); \
(type *) ( (char*)__mptr - offsetof(type,member)); \
})

队列

在linux中的通用队列为kfifo,在文件kernel/kfifo.c中实现。提供了两个主要的操作:入列和出队。另外,kfifo对象还维护了两个偏移量:入口偏移和出口偏移。

enqueue操作会拷贝数据到队列中的入口偏移位置,然后入口偏移位置会增加元素数目的值。dequeue操作则是相反的操作。

映射

linux的映射不是一个通用的映射,它将一个唯一的标识树(UID)映射到一个指针

深度探索C++对象模型<九>

发表于 2018-07-16

Member的各种调用方式

Nonstatic Member Functions

由于C++的一个设计标准就是使得nonstatic member function要有和一般的nonmember function有相同的效率;

这就带来了一个变化,就是编译器内部会将nonstatic member function转换为等价的nonmember function实体;

转化步骤:

  • 改写函数的原型,安插一个新的参数,即this指针;
  • 将每一个对nontatic data member的存取操作改写为通过this指针的操作;
  • 将member function改写成一个外部函数,修改函数名称;

例如一个这样的函数:

1
2
3
4
5
6
7
8
Point3d Point3d::normalize() const
{
register float mag = magnitude();
Point3d normal;
normal.x = x/mag;
normal.y = y/mag;
normal.z = z/mag;
}

将可能会改写成:

1
2
3
4
5
6
void normalize_7Point3dFv(register const Point3d *const this, Point3d &__result)
{
register float mag = this->magnitude();
__result.Point3d::Point3d(this->x/mag, this->y/mag, this->z/mag);
return;
}

特别的,编译器内部会对名称进行mangling处理,虽然目前处理方式没有统一的标准,但一般来说会在member名称前面加上类名,甚至为了保证重载的操作,会加上函数参数的类型,当然如果声明了extern C,就会抑制了这种效果,这也是extern关键字的一个重要功能。

有时我们看到的编译器报错,显示了非常奇怪的函数名称报错,往往就是因为name mangling的原因。

Virtual member functions

如果normalize()是一个虚函数,那么以下调用会转化为:

1
2
ptr->normalize();
(*ptr->vptr[1])(ptr);

1就是virtual function slot的索引值,关联到normalize()函数。

而如果里面的magnitude()函数也是虚函数,那么由于normalize会先调用,决定了object的类型,所以编译器会使用更加明确的调用方式,而不是(*ptr->vptr[2])(ptr);例如:

1
register float mag = Point3d::magnitude();

Static Member Functions

如果normalize是一个静态函数,那么这两种调用会变为:

1
2
3
4
obj.normalize();
prt->normalize();
//会转为
normalize_7Point3dSFv();

由于static member function没有this指针,所以:

  • 不能直接存取其中class的nonstatic members;
  • 不能被声明为const, volatile或者virtual;
  • 不需要经由对象调用(但是如果static member是一个private,就很可能要依赖于对象);

如果要取一个static member function的地址,那么其类型会是:unsigned int (*)();

Virtual Member Functions

为了支持virtual function机制,必须要有某种策略能够在运行期进行类型判断。考虑这样的一个调用:

1
ptr->z()

这样的一个调用需要两个信息:

  • 指针指向的对象的地址;
  • 对象类型的某种编码或者某种结构的地址;

为了提高效率,不支持多态的类是不需要这些额外的信息的。因此我们可以通过类中是否含有virtual functions判断是否支持多台。

那么virtual function是如何被构建的呢?每一个类都会有一个virtual table,而每一个table内含其对应的类对象所有虚函数的地址,每一个虚函数都会被指派一个索引值。而每个类对象都会被编译器安插一个指针,指向该virtual table。

一个类继承基类的时候:

  • 继承所有的虚函数实体,将这些函数实体的地址拷贝到派生类的虚函数表的slot中;
  • 填写自定义的虚函数地址到slot中;
  • 新加的虚函数,新加一个slot;

多重继承下的virtual function

考虑这样的继承关系:

derived : base1, base2

这种多重继承的关键在于base2 subobject的身上:假如为派生对象指派一个base2的指针:

1
Base2 *pbase2 = new Derived;

那么在编译器需要调整指针的指向,使其指向base2 subobject的位置。不然是无法通过指针调用。

指向member function的指针

前面我们提到过,取一个nonstatic data menber的地址,如果该函数是nonvirtual的,则得到的是它在内存中的地址,但是这个地址也是依赖于对象地址的。

事实上,一个member function的指针,如果不用于virtual function这些情况的话,它的效率并不会比使用一个nonmember function的指针更高

支持指向virtual member functions的指针

考虑这样的一个程序片段:

1
2
float (Point::*pmf)() = &Point::z;
Point *ptr = new Point3d;

那么无论是通过prt去调用,还是通过pmf的间接调用,虚拟机制都是有效的:

1
2
ptr->z();
(ptr->*pmf)();

上一节提到过,对一个nonstatic member function取地址,得到的是该函数在内存中的地址,然而对于一个virtual function来说,其地址在编译器是位置的,因此如果对这样的函数取地址,返回的将会是一个索引值。

1
2
3
4
5
6
7
8
9
class Point
{
public:
virtual ~Point();
float x();
float y();
virtual float z();
//...
};

进行各种取地址操作:

1
2
3
4
&Point::~Point; //1
&Point::x();//返回的是这两个函数在内存中的地址
&Point::y();
&Point::z();//2

多重继承下,指向member function的指针

为了让指向member functions的指针能够支持多重继承和虚拟继承,c++设计了这样一个结构:

1
2
3
4
5
6
7
8
struct __mptr{
int delta;//this指针的offset值
int index;//virtual table的索引
union {
ptrtofunc faddr;//nonvirtual member function地址
int v_offset;//virtual base class的vptr的位置
};
};

Inline Functions

一个inline函数,在被编译器处理的过程中,有两个阶段:

  • 分析函数定义,判断函数的intrinsic inlien ability。如果函数因其复杂性,或者内部的构建问题,被判断为不可inline,那么它会被转为一个static函数,然后在被编译的模块中产生相应的函数定义;
  • 真正的inline函数扩展是在调用的那一点上,这会产生参数的求值操作和临时性的对象管理。

formal arguments

在inline函数扩展期间,每一个形参都被实参所代替,但如果参数有副作用,就不能简单地替换,因为这可能会导致实参多次求值。为了避免这个问题,通常会引进临时对象。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
inline int min(int i, int j)
{
return i < j ? i : j;
}

inline int bar()
{
int minval;
int val1 = 1024;
int val2 = 2048;
/* 1 */ minval = min(val1, val2);
/* 2 */ minval = min(1024, 2048);
/* 3 */ minval = min(foo(), bar()+1);
return minval;
}

//第一个 参数直接替换
minval = val1 < val2 ? val1 : val2;
//第二个 直接使用常量
minval = 1024;
//第三个 引入临时对象
int t1, t2;
minval = (t1 = foo()), (t2 = bar()+1), t1<t2 ? t1 : t2;

局部变量

如果inline函数有局部变量,为了避免命名冲突,inline函数的每一个局部变量都必须被封装在函数调用的一个scope中,拥有独一无二的名称。

inline函数是#define的一个安全替代品,避免了宏中出现副作用参数的问题。但如果一个inline函数被调用多次,也会产生大量的扩展码。

Linux内核学习——系统调用

发表于 2018-07-15

系统调用

在操作系统中,内核提供了用户进程与内核进行交互的一组接口。

与内核通信

系统调用在用户空间与硬件设备之间添加了一个中间层,该层主要有三个作用:

  • 提供硬件的抽象接口,这样用户就不需要理会硬件类型;
  • 保障系统的稳定和安全,内核可以基于权限等规则去对用户的访问进行裁决;
  • 因为每个进程都运行在虚拟内存中,如果允许用户进程随意访问硬件,就无法实现多任务和虚拟内存了;

API, POSIX和C库

API定义了一组应用程序使用的编程接口,它可以是一个系统调用,也可以是通过多个系统调用来实现。C库实现了Unix系统的主要API,包括标准C库函数和系统调用接口。

系统调用

通常我们会通过C库中定义的函数来调用系统调用(syscall)。系统调用往往具有一个明确的操作,例如getpid()系统调用,它会返回当前进程,在内核中的实现为:

1
2
3
4
5
6
SYSCALL_DEFINE0(getpid)
{
return task_tgid_vnr(current);//这个宏定义了一个无参数的系统调用,因此这里数字为0
}
//展开后
asmlinkage long sys_getpid(void)

函数声明中的asmlinkage为一个编译指令,通知编译器仅仅从栈中提取参数,其次,为了保证32位和64位的兼容,应该返回long(调用失败返回负数,返回0通常表明成功)。另外,在调用出现错误的时候,C库会把错误码写入errno这个全局变量。

系统调用号

在Linux中,每个系统调用都被赋予一个系统调用号,而且系统调用号不能变更,也不能删除。另外,为了避免一个系统调用被删除或者出现问题后不可用,Linux用一个“未实现”的系统调用sys_ni_syscall()来填补空缺。

内核在arch/i386/kernel/syscall_64.c中为已经注册过的系统调用表存储系统调用号——sys_call_table。

系统调用的性能

Linux系统调用通常非常快:

  • 上下文切换时间较短;
  • 操作本身比较简洁;

系统调用处理程序

由于用户空间的程序无法直接执行内核代码,所以需要某种机制保证用户去通知内核执行某个系统调用。

这个机制是用软中断实现的,通过引发一个异常(int $0x80指令)使得系统切换到内核态去执行异常处理程序(第128号异常处理程序),而这个异常处理程序实际上就是系统调用处理程序——system_call()。

指定恰当的系统调用

刚刚的操作只是切换到内核态,但我们仍然需要告诉内核用户进程的系统调用号,在x86里,该系统调用号是通过寄存器eax传递的。在陷入内核态之前将系统调用号存放到eax里,这样就可以在系统调用程序运行的时候拿到数据。

在拿到系统调用号后,system_call()就会将系统调用号和NR_syscalls作比对,检查有效性。

参数传递

多数的系统调用需要一些外部的参数输入,而这些参数的输入通常也是存放在寄存器里传递的,例如前五个参数通过ebx,ecx,edx,esi和edi这五个寄存器传递,如果参数超过5个,就存放一个指向参数所在用户空间的地址。

返回值也是通过寄存器eax传递。

系统调用上下文

内核在执行系统调用的时候处于进程的上下文,并且在进程上下文中,内核可以休眠且可以被抢占。

LinearModel

发表于 2018-07-12

线性模型

基本形式

线性模型是通过一个属性的线性组合进行预测的函数: \[ f(x) = w_1x_1 + w_2x_2 + ... + w_dx_d + b = w^Tx + b \]

  • 许多非线性模型可以通过在线性模型的基础上引入层级结构或者高维映射形成;
  • 另外,向量w直观地表达了各个属性在预测中的重要性;

线性回归

给定数据集\(D={(x_1, y_1), (x_2, y_2),...,(x_m, y_m)}\),其中向量\(x_i=(x_{i1};x_{i2};...;x_{id})\)。而线性回归就是希望找到一个线性模型能够准确地预测出值。也就是试图学习得: \[ f(x) == w^Tx + b,使得f(x_i)\approx{y_i} \] 那么这里的关键要确定w和b,也就是保证f(x)和y之间最小的时候获得w和b,从几何的角度来看,就是找到一条直线使得所有样本到直线上的欧氏距离之和最小。

假设有 \[ X= \begin{bmatrix} x_{11} & x_{12} & x_{13} & \dots & x_{1d} \\ x_{21} & x_{22} & x_{23} & \dots & x_{2d} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ x_{m1} & x_{m2} & x_{m3} & \dots & x_{md} \end{bmatrix} \]

\[ y = (y_1; y_2;...;y_m) \]

令\(E_w = (y-Xw)^T (y-Xw)\),对\(w\)求导,得 \[ \frac{\alpha{E_w}}{\alpha{w}} = 2X^T(Xw-y) \] 当\(X^TX\)为正定矩阵或者满秩矩阵,令导数为0可得: \[ 2X^T(Xw-y) = 0 \\ X^TXw = X^Ty \\ w = (X^TX)^{-1}X^Ty \] 假如我们认为对应的输入会导致在指数尺度上发生变化,可以转换成对数线性回归: \[ ln y = w^T+b \]

对数几率回归

上面提及的是利用线性模型做回归学习,如果面对分类任务应该怎么做呢?

考虑二分类问题,则\(y\in({0, 1})\),我们需要将线性回归模型产生的值转换为0/1值。则

当z<0,y = 0;

当z=0,y = 0.5;

当z>0,y = 1

因此我们可以sigmoid函数代替,\(y = \frac{1}{1+e^{-z}}\),图像如下:

img
img

带入上式,可以得到: \[ y = \frac{1}{1+e^{-{(w^Tx+b)}}} \\ ln \frac{y}{1-y} = w^Tx+b \] 若把y视为样本x作为正例的可能性,1-y为其反例可能性,那么$ln $就是对数几率。这个方法不仅仅可以预测出类别,更可以得到近似概率的预测,提高决策效果。

当要确定w和b的时候,可以使用极大似然法来估计,目的是使得每个样本属于真实样本的可能性越大越好: \[ \iota(w, b) = \sum_{i=1}^{m}ln p(y_i|x_i;w,n) \] 其中似然项可以写成: \[ p(y_i|x_i;w, b) = y_ip(y=1|x,w;b) + (1-y_i)p(y=0|x,w;b) \] 带入上式之后,使用梯度下降可以求得最优解。

关于极大似然法,可以参考

线性判别分析

linear discriminant analysis(LDA)是一种经典的线性学习方法,可用作分类,也可以为后续的分类做降维操作。

LDA的思想:设法将样本投影到这样的一条直线上:同类的样例的投影点尽可能相近,异类样例的投影点尽可能远离,这样就可以根据投影点的位置来确定新样本的类别。

img
img

https://i.stack.imgur.com/9k7iT.png

令\(X_i, \mu_i, \Sigma_{i}\)分别表示第i类实例的集合、均值向量和协方差矩阵。在将样本值投影到直线之后,两个样本的协方差分别为\(w_T\Sigma_0w\)和\(w_T\Sigma_1w\),而样本中心的投影则是\(w_T\mu_0\)和\(w_T\mu_1\)。因此,为了使得同类样例的投影点尽可能接近,异类样例的投影中心尽可能大,则需要最大化目标函数: \[ J = \frac{\Arrowvert w^T\mu_0-w^T\mu_1\Arrowvert^2_2}{w_T\Sigma_0w+w_T\Sigma_1w} \\ = \frac{w_T(\mu_0-\mu_1)(\mu_0-\mu_1)^Tw}{w_T(\Sigma_0+\Sigma_1)w} \] 分子\(\Arrowvert w^T\mu_0-w^T\mu_1\Arrowvert^2_2\)尽可能大,保证类中心之间距离尽可能大。分母\(w_T\Sigma_0w+w_T\Sigma_1w\)尽可能小,使得同样例的投影点的协方差尽可能小。

多分类学习

多分类学习的任务,可以基于二分类学习进行推广,即将多分类任务拆分为若干个二分类任务进行求解。有三种拆分策略:

  • 一对一:OvO;
  • 一对其余:OvR;
  • 多对多:MvM;

一对一

假设有N个类别,我们分别进行两两配对,那么就会产生N(N-1)/2个二分类任务,则有N(N-1)/2个分类器。那么在测试的时候,对于测试样本就会产生N(N-1)/2个结果。最终结果可以通过投票产生,被预测得最多的类别作为最终分类结果。

一对其余

OvR的主要思路是每次将一个类别作为正例,其余所有类的样例作为反例,产生N个分类器。在测试的时候,若仅有一个分类器预测为正类,则对应的类别标记为最终结果;若有多个分类器预测为正类,则需要考虑分类器的预测置信度,选择置信度最大的作为分类结果。

多对多

多对多的思路是每次选取若干个类作为正类,若干个其它类作为反类,显然OvO和OvR都是MvM的特例,这里介绍一种最常见的MvM技术:纠错输出码(ECOC)。

ECOC主要分为两个过程:

  • 编码:对N个类别做M次划分,每次将一部分类别作为正类,一部分作为反类。这样就形成了M个训练集和M个分类器。
  • 解码:M个分类器分别对测试样本进行预测,这些预测标记组成一个新的编码,将这个编码与各个类别的编码进行比较,返回其中距离最短的类别作为最终结构。

类别的划分通过编码矩阵来指定,常见形式有二元码和三元码:

img
img

三元码的0表示\(f_i\)分类器不使用该类样本。

纠错输出码对分类器的错误有一定的容忍和纠正能力,假设某个分类器出现了错误,基于这个编码仍能产生正确的分类的结果。另外ECOC编码越长,或者任意两个类别之间的编码距离越远,它的纠错能力就越强。

类别不平衡问题

前面几章介绍的分类学习方法都有一个基本的前提,那就是不同类别的训练样本树木相当。但假设反例有998个,正例有2个,那么学习方法只要每次都返回一个永远将样本预测设为反例的学习器,那么精确度就有99.8%。但显然,这种学习没有意义。

线性分类时,当我们用\(y = w^Tx+b\)对新样本x进行分类时,事实上是用y跟一个阈值进行比较。实际上y代表了正例的可能性,那么几率\(\frac{y}{1-y}\)代表了正例与反例的可能性比值,则: \[ 若\frac{y}{1-y} > 1,预测为正例 \] 然而当训练集正反例数目不等时,令\(m^+\)为正例数目,\(m^-\)为反例数目,那么观测几率就是\(\frac{m^+}{m^-}\),这是基于真实样本是无偏差采样的。因此,我们可以对类别平衡的样本进行一次“再缩放”。 \[ \frac{y}{1-y} = \frac{y}{1-y} \times \frac{m^+}{m^-} \] 现有技术对于这种类别平衡问题有三种做法:

  • 欠采样:去掉一些反例,使得正反例数目接近;
  • 过采样:增加一些正例,使得正反例数目接近;
  • 阈值移动:就是刚刚的那个再缩放;

Linux内核学习——进程调度

发表于 2018-07-11

进程调度

调度程序负责对哪个进程投入运行,运行多久做出决策。通过有效地调度程序,最大化地利用系统资源,即利用处理器的时间。

多任务

多任务操作系统可以同时地并行交互执行多个进程。多任务系统分为两种:抢占式和非抢占式,linux提供了抢占式的多任务模式。

Linux的调度策略

在2.6内核之后,Linux引入了著名RSDL和CFS作为新的进程调度算法。

策略

IO消耗型和处理器消耗型的进程

进程可以分为IO消耗型和处理器消耗型。前者需要更多的IO资源,比如多数的图形交互程序;后者则把时间花在了执行代码上。linux系统为了保证交互式应用和桌面的性能,更倾向于有限调度IO消耗型的进程。

进程优先级

优先级高的进程先获得处理器时间,低的后运行,相同的优先级按照轮转的方式进行调度。

linux采用了两种不同的优先级范围:

  • nice值,[-20-19],默认值为0,高nice值则优先级低,低nice值则属于高优先级,可以通过命令ps -el查看进程列表;
  • 实时优先级,[0-99],与nice值意义相反,99代表最高优先级。另外任何的实时进程的优先级都高于普通进程;

时间片

IO消耗型不需要长的时间片,避免影响交互。处理器型的则希望时间片越长越好。Linux系统的CFS调度器将对处理器的使用比例划分给进程,使得进程获得的时间片长度与系统负载相关。

另外,Linux的CFS调度器抢占处理器的要求是:新进程消耗的使用比比当前进程小,则新进程立刻投入运行。否则,将推迟进行。

Linux调度算法

调度器类

Linux的调度器是以模块化的方式提供,这样不同类型的进程就可以采用不同的调度算法。

例如CFS就是一个针对普通进程的调度类。

公平调度

CFS在调度时,针对每一个进程分配它们1/n的处理器时间——n就是可运行进程的数量。在这种情况下,当可执行任务数量趋于无限时,各自获得的处理器使用时间就会趋近于0,这种情况下会带来高昂的切换消耗。因此CFS为每个进程获得时间片设置一个最小粒度,默认为1ms。

另外,按照CFS的调度策略,任何nice值对应的绝对时间不再是一个绝对值,而是处理器的使用比。

Linux调度的实现

CFS的具体实现代码位于文件kernel/sched_fair.c中。

时间记账

CFS虽然没有时间片的概念,但不变的是它也必须维护每个进程运行的时间记账,确保进程只在公平分配给它的处理器时间内运行。CFS使用了一个结构体struct sched_entity进行追踪。

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
struct sched_entity {
struct load_weight load; /* for load-balancing */
struct rb_node run_node;
struct list_head group_node;
unsigned int on_rq;

u64 exec_start;
u64 sum_exec_runtime;
u64 vruntime;
u64 prev_sum_exec_runtime;


u64 nr_migrations;


#ifdef CONFIG_SCHEDSTATS
struct sched_statistics statistics;
#endif


#ifdef CONFIG_FAIR_GROUP_SCHED
int depth;
struct sched_entity *parent;
/* rq on which this entity is (to be) queued: */
struct cfs_rq *cfs_rq;
/* rq "owned" by this entity/group: */
struct cfs_rq *my_q;
#endif


#ifdef CONFIG_SMP
/*
* Per entity load average tracking.
*
* Put into separate cache line so it does not
* collide with read-mostly values above.
*/
struct sched_avg avg ____cacheline_aligned_in_smp;
#endif
};

而task_struct 包含关于 sched_entity 和 sched_class 这两种结构的信息:

1
2
3
4
5
6
7
8
struct task_struct { /* Defined in 2.6.23:/usr/include/linux/sched.h */
....
struct prio_array *array;
struct sched_entity se;
struct sched_class *sched_class;
....
....
};
  • 虚拟实时

观测上面的结构体,可以看到一个成员vruntime,该变量用来存放进程的虚拟运行时间。

简单来说,CFS通过每个进程的虚拟时间来衡量哪个进程最值得被调用,而这个时间是通过进程的实际运行时间和进程的权重计算出来的。该变量记录了一个程序到底运行了多久以及它还要运行多久。

一个进程的权重越大,表示这进程越需要被运行,因此它的虚拟运行时间越小。

所有的虚拟时间的计算都在update_curr进行:

  • 首先计算进程当前时间与上次启动时间的差值;
  • 通过符合权重与当前时间模拟出进程的虚拟允许时钟;
  • 重新设置cfs的min_vruntime;

进程选择

刚刚已经提到,CFS的核心是在选择下一个运行进程时,选择vruntime值最小的进程。而在linux中,是通过红黑树来组织这些进程队列的。

  • 挑选下一个进程

红黑树存储了所有可运行的进行,而节点的键值就是可运行进程vruntime。那么对应的所有进程中vruntime最小的就是树中最左侧的树叶子节点。

事实上,挑选结点的时候并不是遍历整棵树,而是利用缓存在rb_leftmost字段的值。

  • 向树中加入进程

发生在进程变为可运行状态或者通过fork()第一次创建的时候

  • 从树中删除进程

发生在进程堵塞或者终止时

调度器入口

调度器的入口是在文件kernel/sched.c中的schedule(),该函数先找到一个最高优先级的调度类,然后询问该调度类下一个该运行的进程是什么。

睡眠和唤醒

在等待诸如文件IO或者键盘输入的事件时,进程可能进入休眠状态,变得不可执行——TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE。前者如果接收到一个信号,会被唤醒并响应信号。

等待队列

等待某些事件发生的进程会组成一个简单的链表,这就是等待队列。

  • 调用宏DEFING_WAIT()创建一个等待队列的项;
  • 通过add_wait_queue()将该项加入等待队列;
  • 调用prepare_to_wait()变更进程状态;
  • 如果状态被设置为TASK_INTERRUPTIBLE时,进程可以被信号唤醒;
  • 当进程被唤醒的时候,它会检查条件是否为真,真则退出循环;否则,不断调用schedule()重复该步骤;
  • 进程被唤醒后,进程修改状态,并将自己移除等待队列;

唤醒

唤醒操作通过wait_up()实现,它会唤醒指定队列上的所有进程。为了避免虚假唤醒,所以有时需要一个循环处理保证它等待的条件真正达成。

抢占和上下文切换

上下文切换,把一个可执行进程切换到另一个可执行进程,具体实现在kernel/sched.c的context_switch()。所以新的进程被投入使用时,schedule()就会调用该函数;

  • 负责切换虚拟内存到新的进程中;
  • 负责切换上一个进程的处理器状态到新进程中,包括栈信息和寄存器信息;

在这种情况下,内核需要知道什么时候可以调用schedule(),这是为了避免客户程序调用该函数,导致永远执行。所以我们需要一个need_resched的标识来识别是否需要重新执行一次调度。

每个程序都包含一个need_resched,这样可以更快地访问这个标识(在thread_info)中。

用户抢占

用户抢占发生在:

  • 从系统调用返回用户空间;
  • 从中断处理程序返回用户空间;

内核抢占

内核抢占需要保证重新调度是安全的。linux的解决方法是,如果没有锁,就可以进行抢占,至于锁的使用,就是为每个进程的thread_info假如preempt_count计数器。当计数器为0时,内核就可以抢占。

内核的抢占发生在:

  • 中断处理程序返回内核空间之前;
  • 内核代码再一次具有可抢占性的时候;
  • 显示调用schedule()或者任务阻塞;

实时调度策略

linux提供了两种实时调度策略:SCHED_FIFO和SCHED_RR。这两种策略在学操作系统的时候应该讲过,就不赘述了。

一个是先进先出,一个是round robin

SGI STL中的copy算法

发表于 2018-07-09

SGI STL中的copy算法

在stl中,copy是一个调用非常频繁的函数,而这些操作又涉及到了各种的内存操作——运用assignment operator或者copy constructor。为了提高效率,对于拥有trivial assignment operator的元素类型,可以使用直接的内存复制(例如C库函数——memmove或者memcpy)。

copy算法脉络:

img
img

在这里有一个需要注意的问题,就是由于copy算啊是将输入区间内的元素复制到输出区间中,因此当输入区间与输出区间重叠的时候很有可能出现问题。

img
img

这里出现问题的原因是由于deque的迭代器不是原生指针,如果使用的是vector,迭代器是原生指针,调用copy算法后会以memmove执行实际的复制操作,这样整块内存进行复制就不会出错。

但是这个在我的机器上运行是正确的

gcc version 5.4.0

my_copy_overlap.cpp

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
#include <iostream>
#include <iterator>
#include <algorithm>
#include <deque>

using namespace std;

void print(int v[])
{
for(int i=0;i<9;i++)
cout << v[i] << " ";
cout << endl;
}

int main()
{
{
int ia[] = {0,1,2,3,4,5,6,7,8};
copy(ia+2, ia+7,ia);//输出区间的终点与输入区间重叠,copy正常
print(ia); //2 3 4 5 6 5 6 7 8
}
{
int ia[] = {0,1,2,3,4,5,6,7,8};
deque<int> id(ia, ia+9);
deque<int>::iterator first = id.begin();
deque<int>::iterator last = id.end();
++++first; //advance 2
cout << *first << endl;
----last; //advance -2
cout << *last << endl;
deque<int>::iterator res = id.begin();
copy(first, last, res);
copy(id.begin(), id.end(), ostream_iterator<int>(cout, " "));//2 3 4 5 6 5 6 7 8
cout << endl;
}
{
int ia[] = {0,1,2,3,4,5,6,7,8};
deque<int> id(ia, ia+9);
deque<int>::iterator first = id.begin();
deque<int>::iterator last = id.end();
++++first; //advance 2
cout << *first << endl;
----last; //advance -2
cout << *last << endl;
deque<int>::iterator res = id.begin();
res = res+4;
copy(first, last, res);
copy(id.begin(), id.end(), ostream_iterator<int>(cout, " "));//0 1 2 3 2 3 4 5 6
cout << endl;
}
}

我们还是回到SGI的STL吧,以下是唯一的对外接口:

1
2
3
4
5
template <class InputIterator, class OutputIterator>
inline OutputIterator copy(InputIterator first, InputIterator last, OutputIterator result)
{
return __copy_dispatch<InputIterator, OutputIterator>()(first, last, result);
}

另外,会有两个重载函数,针对原始指针——const char和const wchar_t,进行内存直接拷贝操作:

1
2
3
4
5
6
7
8
9
10
11
inline char* copy(const char* first, const char* last, char* result)
{
memmove(result, first, last-first);
return result+(last-first);
}

inline wchar_t* copy(const wchar_t* first, const wchar_t* last, wchar_t* result)
{
memmove(result, first, sizeof(wchar_t)*(last-first));
return result+(last-first);
}

接下来,我们看一下copy()函数中调用的__copy_dispatch():

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
template <class InputIterator, class OutputIterator>
strcut __copy_dispatch
{
OutputIterator operator()(InputIterator first, InputIterator last, OutputIterator result){
return __copy(first, last, result, iterator_category(first));
}
}

//另外,提供两个偏特化版本,针对的是T*指针类型
//这里的目的是在参数为原生指针的前提下,判断指针所指对象是否拥有trivial assignment operator,如果是,则直接使用memmove进行内存拷贝,否则,需要调用assignment operator进行复制
template <class T>
strcut __copy_dispatch<T*, T*>
{
T* operator()(T* first, T* last, T* result){
typedef typename __type_traits<T>::has_trivial_assignment_operator t;
return __copy_t(first, last, result, t());
}
}
template <class T>
strcut __copy_dispatch<const T*, T*>
{
T* operator()(const T* first, const T* last, T* result){
typedef typename __type_traits<T>::has_trivial_assignment_operator t;
return __copy_t(first, last, result, t());
}
}

先来看看泛化版本的**__copy_dispatch调用的** 函数**__copy**,该函数针对不同类型的迭代器使用的循环条件不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <class InputIterator, class OutputIterator>
inline OutputIterator __copy(InputIterator first, InputIterator last, OutputIterator result, input_iterator_tag)
{
for (; first!=last; ++result, ++first)
*result = *first;
return result;
}

template <class RandomAccessIterator, class OutputIterator>
inline OutputIterator __copy(RandomAccessIterator first, RandomAccessIterator last, OutputIterator result, random_access_iterator_tag)
{
//抽出这个函数,是因为接下来也有被使用的地方
return __copy_d(first, last, result, distance_type(first));
}

接下来是__copy_d的实现:

1
2
3
4
5
6
7
template <class RandomAccessIterator, class OutputIterator, class Distance>
inline OutputIterator __copy_d(RandomAccessIterator first, RandomAccessIterator last, OutputIterator result, Distance*)
{
for (Distance n = last-first; n>0; --n, ++result, ++first)
*result = *first;
return result;
}

回到__copy_dispatch的偏特化版本中,这个函数里面调用 __copy_t()

1
2
3
4
5
6
7
8
9
10
11
12
13
template <class T>
inline T* __copy_t(const T* first, const T* last, T* result, __true_type)
{
memmove(result, first, sizeof(T)*(last-first));
return result+(last-first);
}

template <class T>
inline T* __copy_t(const T* first, const T* last, T* result, __false_type)
{
//原生指针是一种RandomAccessIterator,所以交给了__copy_d
return __copy_d(first, last, result, (ptrdiff_t*)0);
}

即便自定义的class C具备trivial operator=,它仍然可能被判断__false_type,这是因为<type_triais.h>中只针对某些类型做了记录。

<i class="fa fa-angle-left"></i>1…151617…28<i class="fa fa-angle-right"></i>

278 日志
29 标签
RSS
© 2025 LucienXian
由 Hexo 强力驱动
主题 - NexT.Pisces