一、Linux进程空间
1.1 内核空间和用户空间
Linux采用两级保护机制:0级供内核使用、3级供用户程序使用。在32位Linux操作系统中,每个进程都有各自的私有用户空间(0~3GB),这个空间对系统中的其它进程是不可见的,最高的1GB虚拟内核空间为所有进程以及内核所共享。
针对linux操作系统而言:
- 将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间;
- 而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间;
每个进程可以通过系统调用进入内核,因此,Linux内核由系统内的所有进程共享。
需要注意的是:内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。不管是内核空间还是用户空间,它们都处于虚拟空间中。 虽然内核空间占据了每个虚拟空间中的最高1GB字节,但映射到物理内存却总是从最低地址(0x00000000),另外, 使用虚拟地址可以很好的保护 内核空间被用户空间破坏,虚拟地址到物理地址转换过程有操作系统和CPU共同完成(操作系统为CPU设置好页表,CPU通过MMU单元进行地址转换)。
1.2 进程内存布局
Linux进程标准的内存段布局,如下图所示,地址空间中的各个条带对应于不同的内存段(memory segment),如:堆、栈之类的。
1.3 内核态和用户态
- 当一个任务(进程)执行系统调用而陷入内核代码中执行时,称进程处于内核运行态(内核态)。此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈;
- 当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。此时处理器在特权级最低的(3级)用户代码中运行。当正在执行用户程序而突然被中断程序中断时,此时用户程序也可以象征性地称为处于进程的内核态。因为中断处理程序将使用当前进程的内核栈;
程序在执行过程中通常有用户态和内核态两种状态,CPU对处于内核态根据上下文环境进一步细分,因此有了下面三种状态:
- 内核态,运行于进程上下文,内核代表进程运行于内核空间;
- 内核态,运行于中断上下文,内核代表硬件运行于内核空间;
- 用户态,运行于用户空间;
1.4 进程上下文
所谓的“进程上下文”,可以看作是用户进程传递给内核的这些参数以及内核要保存的那一整套的变量和寄存器值和当时的环境等。
相对于进程而言,就是进程执行时的环境。具体来说就是各个变量和数据,包括所有的寄存器变量、进程打开的文件、内存信息等。一个进程的上下文可以分为三个部分:
- 用户级上下文: 正文、数据、用户堆栈以及共享存储区;
- 寄存器上下文:通用寄存器、程序寄存器(PC)、处理器状态寄存器(CPSR)、栈指针(SP);
- 系统级上下文:进程控制块task_struct、内存管理信息(mm_struct、vm_area_struct、pgd、pte)、内核栈;
当发生进程调度时,进行进程切换就是上下文切换(context switch),操作系统必须对上面提到的全部信息进行切换,新调度的进程才能运行。
而系统调用进行的模式切换(mode switch)。模式切换与进程切换比较起来,容易很多,而且节省时间,因为模式切换最主要的任务只是切换进程寄存器上下文的切换。
1.5 中断上下文
硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的 一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。
所谓的“ 中断上下文”,其实也可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被打断执行的进程环境)。
中断时,内核不代表任何进程运行,它一般只访问内核空间,而不会访问进程空间,内核在中断上下文中执行时一般不会阻塞。
二、Linux进程中同步
2.1 进程同步问题
我们的Linux操作系统是一个多任务操作系统,运行在操作系统上的进程宏观上并行运行的,这样系统就会产生一些问题,比如有的资源、比如显示器,CPU同一时间肯定只能有一个程序在使用,多个程序肯定不能同时使用显示器,这就是互斥关系。为了解决这种问题,当一个进程获取到资源后,另一个进程必须等待,等到进程释放资源后,另一个进程才可使用,这就是同步关系。
2.2 临界资源
像上面我们所说的一次只能被一个进程所占用的资源就是临界资源,典型的临界资源比如物理上的CPU、显示器、打印机,或是存在硬盘或内存中被多个进程所共享的一些变量和数据等(如果这类资源不被看成临界资源加以保护,那么很有可能造成丢数据的问题)。
对于临界资源的访问,必须是互诉进行。也就是当临界资源被占用时,另一个申请临界资源的进程会被阻塞,直到其所申请的临界资源被释放。而进程内访问临界资源的代码被成为临界区。
对临界资源的互斥访问了逻辑上可以分为四个部分:
- 进入区:负责检查是否可进入临界区,如果可进入,则应该设置正在访问临界资源的标志(“上锁”),以阻止其他进程同时进入临界区;
- 临界区:访问临界资源的代码;
- 退出区:负责解除正在访问临界资源的标志(“解锁”);
- 剩余区:做其他处理;
注意:进入区和退出区是负责实现互斥的代码段。
临界区准则:
- 有空让进:当无进程在互斥区时,任何有权使用互斥区的进程可进入;
- 无空等待:不允许两个以上的进程同时进入互斥区;
- 多中择一:当没有进程在临界区,而同时有多个进程要求进入临界区,只能让其中之一进入临界区,其他进程必须等待;
- 有限等待:任何进入互斥区的要求应在有限的时间内得到满足;
- 让权等待:处于等待状态的进程应放弃占用 CPU;
- 平等竞争:任何进程无权停止其它进程的运行,进程之间相对运行速度无硬性规定;
2.2 进程同步解决方案
为了能够有效的控制多个进程之间的沟通过程,保证沟通过程的有序和和谐,OS必须提供一定的同步机制保证进程之间不会自说自话而是有效的协同工作。常用的同步方式有:
- 互斥锁;
- 原子操作;
- 条件变量;
- 自旋锁
- 信号量;
三、信号量(semaphore)
3.1 什么是信号量
信号量本质上是一个计数器,它用来记录对某个资源的存取状态。一般来说,为了获取共享资源,进程需要执行下列操作:
- 测试控制该资源的信号量;
- 如果信号量的值为正,则允许操作该资源,并且信号量值减1;
- 如果信号量为0,则资源目前不可用,进程进入休眠状态,直至信号量值大于0,进程被唤醒,转入上一步;
- 当进程不在使用一个信号量控制的资源时,信号量的值加1;
3.2 信号量的工作原理
针对信号量只能进行两种操作,即PV操作,PV操作由P操作原语和V操作原子组成(原子是不可中断的过程),对信号量进行操作,具体定义如下:
- P(s):如果s的值大于零,就给它减1;如果它的值为零,则将该进程设置为等待状态,排入等待队列;
- V(s):如果有其它进程因等待s而被挂起,就让它恢复运行,如果没有进程因等待s而挂起,就给它加1;
3.3 信号量的使用
维护信号量状态的是Linux内核操作系统而不是用户进程。我们可以从头文件/linux-5.2.8/include/linux/semaphore.h:中看到内核用来维护信号量状态的各个结构的定义。信号量结构体定义:
/* Please don't access any members of this structure directly */ struct semaphore { raw_spinlock_t lock; unsigned int count; struct list_head wait_list; };
DEFINE_SEAMPHORE宏用于定义一个信号量,并设置信号量的值为1:
DEFINE_SEAMPHORE(name)
sema_init 函数用于初始化信号量,并设置信号量sem的值为val:
void sema_init (struct semaphore *sem, int val);
down函数用于获得信号量sem,它会导致进程睡眠,不能在中断中使用,不然会导致中断处理程序休眠(该函数目前已不建议使用):
int down(struct semaphore * sem);
down_interruptible函数功能与down类似,只是使用 down 进入休眠状态的线程不能被信号打断。而使用此函数进入休眠后,进程状态被设置为TASK_INTERRUPTIBLE,该类型的睡眠是可以被信号打断的。
如果返回0,表示获得信号量;如果被信号打断,返回EINTR。
int down_interruptible(struct semaphore * sem);
down_trylock函数尝试获得信号量sem,如果能够立刻获得,它就获得该信号量并返回0,否则,返回正数。它不会导致调用者睡眠,可以在中断上下文使用:
int down_trylock(struct semaphore * sem);
up函数释放信号量sem,唤醒等待者。
void up(struct semaphore * sem);
3.4 信号量的实现源码
四、信号量示例程序
4.1 驱动程序
#include <linux/module.h> #include <linux/cdev.h> #include <linux/fs.h> #include <linux/semaphore.h> #define OK (0) #define ERROR (-1) /* 信号量 */ static DEFINE_SEMAPHORE(sema); int hello_open(struct inode *p, struct file *f) { if(down_trylock(&sema) > 0){ printk("device busy,hello_open failed"); return ERROR; } printk("hello_open\n"); return 0; } ssize_t hello_write(struct file *f, const char __user *u, size_t s, loff_t *l) { printk("hello_write\n"); return 0; } ssize_t hello_read(struct file *f, char __user *u, size_t s, loff_t *l) { printk("hello_read\n"); return 0; } int hello_close(struct inode *inode, struct file *file) { /* 释放信号量 */ up(&sema); return 0; } struct file_operations hello_fops = { .owner = THIS_MODULE, .open = hello_open, .read = hello_read, .write = hello_write, .release = hello_close, }; dev_t devid; // 起始设备编号 struct cdev hello_cdev; // 保存操作结构体的字符设备 struct class *hello_cls; int hello_init(void) { /* 动态分配字符设备: (major,0) */ if(OK == alloc_chrdev_region(&devid, 0, 1,"hello")){ // ls /proc/devices看到的名字 printk("register_chrdev_region ok\n"); }else { printk("register_chrdev_region error\n"); return ERROR; } cdev_init(&hello_cdev, &hello_fops); cdev_add(&hello_cdev, devid, 1); /* 创建类,它会在sys目录下创建/sys/class/hello这个类 */ hello_cls = class_create(THIS_MODULE, "hello"); if(IS_ERR(hello_cls)){ printk("can't create class\n"); return ERROR; } /* 在/sys/class/hello下创建hellos设备,然后mdev通过这个自动创建/dev/hello这个设备节点 */ device_create(hello_cls, NULL, devid, NULL, "hello"); return 0; } void __exit hello_exit(void) { printk("hello driver exit\n"); /* 注销类、以及类设备 /sys/class/hello会被移除*/ device_destroy(hello_cls, devid); class_destroy(hello_cls); cdev_del(&hello_cdev); unregister_chrdev_region(devid, 1); return; } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL");
4.2 应用程序
#include <sys/stat.h> #include <fcntl.h> #include <stdio.h> int main(int argc,char **argv) { int fd; int val = 1; fd = open("/dev/hello",O_RDWR); if(fd == -1){ printf("can't open!\n"); }else{ printf("open success,PID=%d\n",getpid()); sleep(10); // 10s } return 0; }
4.3 测试
如下图所示,3个进程同时访问时,有一个进程访问成功,两外两个进程都访问失败了:
[root@zy:/]# ./main &hello_open open success,PID=61 [root@zy:/]# ./main & [root@zy:/]# can't open! [root@zy:/]# ./main & device busy,hello_open failed can't open! [root@zy:/]#
四、互斥量
五、自旋锁
参考文章
[2]Linux进程空间
[5]Linux下进程间通信方式——信号量(Semaphore)