linux内核同步-1

应用程序的同步问题

  • 进程间通信
    • 我们知道,每个进程都有自己的独立地址空间;
    • 进程与进程间的通信必须使用内核提供的方式才能进行;
  • 应用编程的进程间通信有如下方法:
    • 传统的进程间通信方式
      无名管道(pipe)、有名管道(fifo)和信号(signal)
    • System V IPC对象
      共享内存(share memory)、消息队列(message queue)和信号灯(semaphore)
    • BSD
      套接字(socket)
  • 共享内存是进程间最高效的通信方式,其原理如下图:


  • 共享内存的应用程序必须特别留意保护共享资源,防止共享资源被并发访问。
  • 共享资源之所以要防止并发访问,是因为如果多个任务(进程或线程)同时访问和操作数据,就有可能发生各任务之间相互覆盖共享数据的情况,造成被访问数据处于不一致状态。
  • 并发访问共享数据是造成系统不稳定的一类隐患,而且这种错误一般难以跟踪和调试—所以首先应该认识到这个问题的重要性。

什么是临界区和竞争条件

  • 临界区
    • 访问和操作共享数据的代码段;
    • 多个线程同时访问共享的资源会存在不安全性;
    • 代码在执行结束前不可被打断,就如同整个临界区是一个不可分割的指令一样。
  • 竞争条件(bug)
    • 两个线程同时进入同一个 临界区会发生
  • 同步
    • 保证不安全的并发不发生,竞争条件不发生
  • 如何防止竞争条件?
    禁止多个进程同时访问共享资源。
    没有两个进程同时处于临界区.
    不应对CPU的速度和数量作任何假设.
    临界区之外的进程不能阻塞其他进程进入临界区.
    不应该有进程永远等待进入临界区.


  • 什么造成了并发?
    中断----随时打断当前正在执行的代码;
    Softirqs和tasklets ----内核能在任何时刻唤醒或调度软中断和tasklet,打断当前正在执行的代码;
    内核抢占----内核中的任务可能会被另一任务抢占;
    用户空间的睡眠和同步 ----在内核执行的进程可能会睡眠,这就会唤醒调度程序,从而导致调度一个新的用户进程执行
    SMP----两个或多个处理器可以同时执行代码;
  • 我们怎么知道什么东西需要保护?
    • 不需要保护
      • 执行线程的局部数据仅仅被它本身访问
      • 如果数据只会被特定的进程访问
  • 在写内核驱动的时候我们需要考虑以下的问题:
    • 是否全局变量?
    • 这些数据是进程上下文和中断上下文之间共享的数据吗?
    • 如果一个进程在访问这些数据时被抢占了,新调度的进程可以访问这些数据吗?
    • 当前进程是否可以睡眠在(阻塞)在一些资源上?
    • 怎样防止数据失控?
    • 这个函数如果在另一个处理器调用,会发生什么事情?

什么是死锁

  • 举一个死锁的例子:


  • 造成死锁的原因:
    多线程多资源
    每一个线程都在等待其中的一个资源
    但所有的资源都已被占用
    所有线程都在相互等待,但它们永远不会释放已经占有的资源。于是任何线程都无法继续,这便发生了死锁。


  • 怎么防止死锁发生呢?
    锁的顺序至关重要
    防止饥饿
    不要对同一个锁上两次锁
    锁的方案过于复杂易导致死锁

原子操作

  • 原子操作可以保证指令以原子的方式执行——执行过程不被打断。
  • 原子原本指的是不可分割的微粒,所以原子操作也就是不能够被分割的指令。
  • 内核提供了两组原子操作接口
    • 一组针对整数进行操作;
    • 另一组针对单独的位进行操作

原子整数操作:

  • 原子整数方法使用一种特殊的类型: atomic_t
  • 引入了一个特殊数据类型主要是出于一下原因:
    首先,让原子函数只接受atomic_t类型的操作数,可以确保原子操作函数只与这种特殊类型数据一起使用。
    同时,这也保证了该类型的数据不会被传递给其他任何非原子函数。
    最后,在不同体系结构上实现原子操作的时候,使用atomic_t可以屏蔽其间的差异。
  • 使用原子整型操作需要的声明都在<asm/atomic.h>文件中。
    使用例子:
  • 原子整数操作最常见的用途就是实现计数器。
atomic_inc(atomic_t *v)  自加1
atomic_dec(atomic_t *v) 自减1
  • 还可以用原子整数操作原子地执行一个操作并检查结果。一个常见的例子就是原子的减操作和检查。
    int atomic_dec_and_test(atomic_t *v);
    这个函数将给定的原子变量减1,如果结果为0,就返回真;否则返回假。
  • 原子操作函数如下:
ATOMIC_INlT(int i); 在声明一个atomic_t变量时,将它初始化为i
int atomic_read(atomic_t *v);   原子地读取整数变量v
void atomic_set(atomic_t *v, int i);    原子地设置v值为1
void atomtic_add(int i,atomic_t *v);    原子地给v加i
void atomic_sub(int i,atomic_t *v); 原子地从v减i
void atomic_inc(atomic_t *v);       原子地给v加1
void atomic_dec(atomic_t *v);       原子地从v减1
int atomic_sub_and_test(int i, atomic_t *v);原子地从v减i,如果结果等于0返回真;否则返回假
int atomtic_add_negative(int i,atomic_t *v); 原子地给v加i,如果结果是负数,返回真;否则返回假
int atomic_dec_and_test(atomic_t *v);   原子地给v减1,如果结果是0,返回真;否则返回假
int atomic_inc_and_test(atomic_t *v);   原子地给v加1,如果结果是0,返回真;否则返回假

原子位操作

  • 原子位操作函数的头文件:<asm/bitops.h>
  • 原子位操作函数是对普通的内存地址进行操作的,没有特定的数据类型。
  • 原子位操作函数的参数是一个指针和一个位号
  • 第0位是给定地址的最低有效位;
  • 在32位机上,第31位是给定地址的最高有效位;
  • 而第32位是下一个字的最低有效位
    使用原子整数操作的例子:
unsigned long word = 0; 
set_bit(0, &word); /* 第0位被设置为1(原子操作) */ 
set_bit(1, &word); /*第1位被设置为1(原子操作) */
printk(“%ul\n”, word); /* 将会打印 "3" */ 
clear_bit(1, &word); /* 第1位被清零 */ 
change_bit(0, &word); /* 第0位被反转,即被清零(原子操作) */ 
/*设置第0位,并返回以前的值(0)*/ 
if (test_and_set_bit(0, &word)) { 
    /* 永远不会为真*/ 
} 
/* 下面的语句是合法的*/ 
word = 7; 
void set_bit(int nr,void *addr) 原子地设置addr所指对象的第nr位
void clear_bit(int nr,void*addr) 原子地清空addr所指对象的第nr位
void change_bit(int nr,void *addr) 原子地翻转addr所指对象的第nr位
int test_and_set_bit(int nr,void*addr)   原子地设置addr所指对象的第nr位,并返回原先的值
int test_and_clear_bit(int nr,void *addr)   原子地清空addr所指对象的第nr位,并返回原先的值
int lest_and_change_bit(int nr,void *addr)   原子地翻转addr所指对象的第nr位,并返回原先的值
int test_bit(int nr,void *addr)   原子地返回addr所指对象的第nr位
  • 为方便起见,内核还提供了一组与上述操作对应的非原子位函数。
  • 非原子位函数与原子位函数的操作完全相同,但是,前者不保证原子性,且其名字前缀多两个下划线。例如,与test_bit()对应的非原子形式是__test_bit()。
  • 如果你不需要原子性操作(比如说,如果你已经用锁保护了自己的数据),那么这些非原子的位函数相比原子的位函数可能会执行得更快些。
  • 内核还提供了两个例程用来从指定的地址开始搜索第一个被设置(或未被设置)的位。
int find_first_bit(unsigned long*addr,unsigned int size)
int find_first_zero_bit(unsigned long*addr,unsigned int size)
  • 这两个函数中第一个参数是一个指针
  • 第二个参数是要搜索的总位数
  • 返回值分别是第一个被设置的(或没被设置的)位的位号
  • 如果你的搜索范围仅限于一个字,使用__ffs()和__ffz()这两函数更好,它们只需要给定一个要搜索的地址做参数。

原子操作实战

驱动代码如下,以点亮led为例子:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/types.h>
#include <linux/uaccess.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/io.h>
#include <asm/atomic.h>

static unsigned long gpm4con;
#define GPM4CON (*(volatile unsigned long *)(gpm4con + 0x0))
#define GPM4DAT (*(volatile unsigned long *)(gpm4con + 0x04))


static atomic_t flag = ATOMIC_INIT(1);
//open
ssize_t led_open(struct inode *inop,struct file *filp)
{
/*  if(atomic_dec_and_test(&flag) == 0)
    {
        atomic_inc(&flag);
        return -EBUSY;
    }
*/
    if(atomic_read(&flag))
        atomic_dec(&flag);
    else
    {
        printk("EBUSY = %d\r\n",EBUSY);
        return -EBUSY;
    }
    
    GPM4CON = 0X1111;
    GPM4DAT &= ~0XF;
    printk("led open!\n");
    return 0;
}

//release
ssize_t led_release(struct inode *inop,struct file *filp)
{
    atomic_inc(&flag);
    GPM4DAT |= 0xf;
    printk("led release!\n");
    return 0;
}


static unsigned char led_state = 0;
//read
ssize_t led_read(struct file *filp,char __user *buf,size_t size,loff_t f_pos)
{
    copy_to_user(buf,&led_state,size);
    printk("led read data %d\n",led_state);
    return size;
}

//write
ssize_t led_write(struct file *filp,const char __user *buf,size_t size,loff_t f_pos)
{
    copy_from_user(&led_state,buf,size);
    printk("led write data %d\n",led_state);
    GPM4DAT=led_state;
    return size;

}

struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .release = led_release,
    .read = led_read,
    .write = led_write,
};


static unsigned int led_major = 0;
static struct class *led_class;
#define LED_DRV_NAME "led_drv"

static int __init led_init(void)
{
    gpm4con=(unsigned int)ioremap(0x110002e0,12);
    led_major=register_chrdev(0,LED_DRV_NAME,&fops);
    led_class=class_create(THIS_MODULE,LED_DRV_NAME);
    device_create(led_class,NULL,MKDEV(led_major,0),NULL,LED_DRV_NAME);
    return 0;
}

static void __exit led_exit(void)
{
    device_destroy(led_class,MKDEV(led_major,0));
    class_destroy(led_class);
    iounmap(gpm4con);
    unregister_chrdev(led_major,LED_DRV_NAME);
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");

分别写两个测试程序来测试:
led1.c代码如下:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>

#define FILE_NAME "/dev/led_drv"
int main(void)
{
    int fd = 0;
    unsigned char val;
    int i = 0,j=0;


    printf("我是进程1!\r\n");
    do{
        fd = open(FILE_NAME,O_RDWR);
        printf("进程1 fd = %d\n",fd);
        sleep(1);
    }while(fd < 0);
    sleep(2);
    printf("led1 open ok!\r\n");

    //left
    val = 0xfe;
    for(i=0;i<3;i++)
    {
        for(j=0;j<4;j++)
        {
            
            printf("led1\n");
            usleep(1);              //应用层输出切换内核输出,加一点延时,否则会执行乱序
            write(fd,&val,1);
            val = val<<1 | val>>7;
            usleep(200000);
        }
    }

    close(fd);
    printf("led1 exit\n");
    return 0;
}

led2.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>

#define FILE_NAME "/dev/led_drv"

int main(void)
{
    int fd = 0;
    unsigned char val;
    int i = 0,j=0;

    printf("我是进程2!\r\n");
    do{
        fd = open(FILE_NAME,O_RDWR);
        printf("进程2 fd = %d\n",fd);
        sleep(1);
    }while(fd < 0);

    sleep(1);
    printf("led2 open ok!\r\n");

    //right
    val = 0xf7;
    for(i=0;i<3;i++)
    {
        for(j=0;j<4;j++)
        {
            printf("led2\n");
            usleep(10);
            write(fd,&val,1);
            val = val>>1 | val<<7;
            usleep(100000);
        }
    }

    close(fd);
    printf("led2 exit\n");
    return 0;
}

makefile如下:

obj-m += led_drv.o
all:
    make -C /home/sice/linux-3.5 M=`pwd` modules
install:
    make -C /home/sice/linux-3.5 M=`pwd` INSTALL_MOD_PATH=/opt/rootfs modules_install
clean:
    make -C /home/sice/linux-3.5 M=`pwd` clean
    rm -rf led1 led2

led:
    arm-linux-gcc led1.c -o led1
    arm-linux-gcc led2.c -o led2

copy:
    cp led1 led2 /opt/rootfs
    cp led.sh /opt/rootfs
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容