Linux采用两级保护机制:0级供内核使用、3级供用户程序使用。在32位Linux操作系统中,每个进程都有各自的私有用户空间(0~3GB),这个空间对系统中的其它进程是不可见的,最高的1GB虚拟内核空间为所有进程以及内核所共享。
针对linux操作系统而言:
每个进程可以通过系统调用进入内核,因此,Linux内核由系统内的所有进程共享。
需要注意的是:内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。不管是内核空间还是用户空间,它们都处于虚拟空间中。 虽然内核空间占据了每个虚拟空间中的最高1GB字节,但映射到物理内存却总是从最低地址(0x00000000),另外, 使用虚拟地址可以很好的保护 内核空间被用户空间破坏,虚拟地址到物理地址转换过程有操作系统和CPU共同完成(操作系统为CPU设置好页表,CPU通过MMU单元进行地址转换)。
Linux进程标准的内存段布局,如下图所示,地址空间中的各个条带对应于不同的内存段(memory segment),如:堆、栈之类的。
程序在执行过程中通常有用户态和内核态两种状态,CPU对处于内核态根据上下文环境进一步细分,因此有了下面三种状态:
所谓的“进程上下文”,可以看作是用户进程传递给内核的这些参数以及内核要保存的那一整套的变量和寄存器值和当时的环境等。
相对于进程而言,就是进程执行时的环境。具体来说就是各个变量和数据,包括所有的寄存器变量、进程打开的文件、内存信息等。一个进程的上下文可以分为三个部分:
当发生进程调度时,进行进程切换就是上下文切换(context switch),操作系统必须对上面提到的全部信息进行切换,新调度的进程才能运行。
而系统调用进行的模式切换(mode switch)。模式切换与进程切换比较起来,容易很多,而且节省时间,因为模式切换最主要的任务只是切换进程寄存器上下文的切换。
硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的 一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。
所谓的“ 中断上下文”,其实也可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被打断执行的进程环境)。
中断时,内核不代表任何进程运行,它一般只访问内核空间,而不会访问进程空间,内核在中断上下文中执行时一般不会阻塞。
我们的Linux操作系统是一个多任务操作系统,运行在操作系统上的进程宏观上并行运行的,这样系统就会产生一些问题,比如有的资源、比如显示器,CPU同一时间肯定只能有一个程序在使用,多个程序肯定不能同时使用显示器,这就是互斥关系。为了解决这种问题,当一个进程获取到资源后,另一个进程必须等待,等到进程释放资源后,另一个进程才可使用,这就是同步关系。
像上面我们所说的一次只能被一个进程所占用的资源就是临界资源,典型的临界资源比如物理上的CPU、显示器、打印机,或是存在硬盘或内存中被多个进程所共享的一些变量和数据等(如果这类资源不被看成临界资源加以保护,那么很有可能造成丢数据的问题)。
对于临界资源的访问,必须是互诉进行。也就是当临界资源被占用时,另一个申请临界资源的进程会被阻塞,直到其所申请的临界资源被释放。而进程内访问临界资源的代码被成为临界区。
对临界资源的互斥访问了逻辑上可以分为四个部分:
注意:进入区和退出区是负责实现互斥的代码段。
临界区准则:
为了能够有效的控制多个进程之间的沟通过程,保证沟通过程的有序和和谐,OS必须提供一定的同步机制保证进程之间不会自说自话而是有效的协同工作。常用的同步方式有:
信号量本质上是一个计数器,它用来记录对某个资源的存取状态。一般来说,为了获取共享资源,进程需要执行下列操作:
针对信号量只能进行两种操作,即PV操作,PV操作由P操作原语和V操作原子组成(原子是不可中断的过程),对信号量进行操作,具体定义如下:
维护信号量状态的是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);
#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");
#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; }
如下图所示,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:/]#
参考文章
[1]Linux进程空间分布 & 上下文
[2]Linux进程空间
[3]操作系统--进程同步和互斥的概念
[4]进程互斥同步及通信死锁问题
[5]Linux下进程间通信方式——信号量(Semaphore)