【driver2】设备读写,同步和互斥,ioctl,进程休眠,时间和延时,延缓

文章目录

  • 1.实现设备读写:write函数中一个进程写没问题,两进程写:第一个进程运行到kzalloc时,第二个进程也执行了kzalloc,只第二个进程地址保存在c中,第一个进程分配内存空间地址丢失造成内存泄漏。第一个进程运行到kzalloc时第二个进程调用了kfree,这时第一个进程执行到copy_from_user出现问题
  • 2.同步和互斥:避免多进程同时操作设备问题
  • 3.实现设备驱动的ioctl函数: nr=number
    • 3.1 hello_chr_locked.h:ioctl的第二个参数即cmd即HC_IOC_RESET,正常情况要不都按值传递,要不都用指针
    • 3.2 hello_chr_locked.c:ioctl执行硬件控制,除了读写设备文件外的其他操作,比如锁门、弹出介质、设置波特率、设置比特位等
    • 3.3 ioctltest.c:测试函数操作ioctl,先insmod hello_chr_locked.ko
  • 4.实现进程休眠:条件不够歇一歇,把CPU让给其他进程
  • 5.内核表示时间和实现延时:linux中有一个时钟会周期性产生中断,linux将这中断作为时间基准
  • 6.内核实现延缓操作:内核定时器,tasklet,workqueue


1.实现设备读写:write函数中一个进程写没问题,两进程写:第一个进程运行到kzalloc时,第二个进程也执行了kzalloc,只第二个进程地址保存在c中,第一个进程分配内存空间地址丢失造成内存泄漏。第一个进程运行到kzalloc时第二个进程调用了kfree,这时第一个进程执行到copy_from_user出现问题

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.同步和互斥:避免多进程同时操作设备问题

如下是不同步的表现。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

// hello_chr_locked.c
/*
	信号量: sema_init()    初始化信号量
		     down_interruptible()    获取信号量
		     up()    释放信号量

	互斥锁: mutex_init()
			 mutex_lock_interruptible()   获取
			 mutex_unlock()
*/
#include<linux/module.h>
#include<linux/fs.h>
#include<linux/cdev.h>
#include<linux/slab.h>
#include<linux/uaccess.h>
#include<linux/jiffies.h>
#include<linux/sched.h>
#include<linux/semaphore.h>  //信号量
#include<linux/mutex.h>      //互斥锁
#define LOCK_USE 1  //0:semaphore, 1:mutex
#define HELLO_MAJOR 0
#define HELLO_NR_DEVS 2

int hello_major = HELLO_MAJOR;
int hello_minor = 0;
dev_t devt;      //高12位是主设备号,低20位是次设备号
int hello_nr_devs = HELLO_NR_DEVS;

module_param(hello_major, int, S_IRUGO);
module_param(hello_minor, int, S_IRUGO);
module_param(hello_nr_devs, int, S_IRUGO);

struct hello_char_dev{		//实际的字符设备结构,类似于面向对象的继承
	struct cdev cdev;
	char *c;
	int n;
	struct semaphore sema;   //信号量
	struct mutex mtx;       //互斥锁 ,实际两者中一个就行
};
struct hello_char_dev *hc_devp;
struct class *hc_cls;

//11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
int hc_open(struct inode *inode, struct file *filp)
{
	struct hello_char_dev *hc_dev;
	printk(KERN_INFO "%s open \n",current->comm);
	hc_dev = container_of(inode->i_cdev,struct hello_char_dev,cdev);  //获取设备结构体的地址
	filp->private_data = hc_dev;		//将设备结构地址放到文件描述符结构的私有数据中

	return 0;
}

ssize_t hc_read(struct file *filp, char __user *buf, size_t count,loff_t *f_pos)
{
	ssize_t retval=0;
	struct hello_char_dev *hc_dev=filp->private_data;
	printk(KERN_INFO "read hc_dev %p\n",hc_dev);
	
	if(*f_pos >= hc_dev->n)
		goto out;
	if(*f_pos + count > hc_dev->n)
		count = hc_dev->n - *f_pos;
	
	if(copy_to_user(buf,hc_dev->c,count))
	{
		retval = -EFAULT;
		goto out;
	}

	*f_pos += count;
	return count;	
out:
	return retval;
}

ssize_t hc_write(struct file *filp, const char __user *buf, size_t count,loff_t *f_pos)
{
	struct hello_char_dev *hc_dev=filp->private_data;
	int retval = -ENOMEM;
	unsigned long jiff1;
	printk(KERN_INFO "%s write begin\n",current->comm);
	#if (LOCK_USE==0)
	if(down_interruptible(&hc_dev->sema))	 //-EINTR   down_interruptible获得信号量,成功获取返回0,执行后面操作。获取不到进行等待,让进程进入休眠状态。如果我们给它发送一个信号(不是信号量),它就会返回一个非0值,这个值就是EINTR,如果接到EINTR就返回ERESTARTSYS去重新获取信号量。如果接到信号后退出,下行就返回EINTR。
		return -ERESTARTSYS;
	printk(KERN_INFO "%s get sema\n",current->comm);
	#endif
	#if (LOCK_USE==1)
	if(mutex_lock_interruptible(&hc_dev->mtx))  //-EINTR  mutex_lock_interruptible获得互斥量,参数同样是互斥量地址,获得到互斥量返回0(不进这个if里)执行后面操作,无法获得互斥量就会让进程进入休眠状态,接收到一个信号返回非0值即-EINTR
		return -ERESTARTSYS; 
	printk(KERN_INFO "%s get mutex\n",current->comm);
	#endif

   //获得到信号量或互斥锁后就可正常执行如下写操作:
	kfree(hc_dev->c);
	hc_dev->c=NULL;
	hc_dev->n=0;
	printk(KERN_INFO"%s 1",current->comm);
	jiff1=jiffies;
	while(jiffies-jiff1<HZ);
	hc_dev->c = kzalloc(count,GFP_KERNEL);
	if(!hc_dev->c)
		goto out;
	printk(KERN_INFO"%s 2 addr:%p",current->comm,hc_dev->c);
	jiff1=jiffies;
	while(jiffies-jiff1<HZ);
	printk(KERN_INFO"%s 3 addr:%p",current->comm,hc_dev->c);
	if(copy_from_user(hc_dev->c,buf,count))
	{
		retval = -EFAULT;
		goto fail_copy;
	}
	hc_dev->n = count;
	
   //如上写操作执行完后,如下我们需要释放掉互斥量或互斥锁:
	#if (LOCK_USE==0)
	up(&hc_dev->sema);   //up释放信号量,参数是信号量地址
	printk(KERN_INFO "%s up sema\n",current->comm);
	#endif
	#if (LOCK_USE==1)
	mutex_unlock(&hc_dev->mtx);  //mutex_unlock释放互斥锁,参数是互斥锁地址
	printk(KERN_INFO "%s unlock mutex\n",current->comm);
	#endif
	printk(KERN_INFO"%s write done",current->comm);
	return count;	 
fail_copy:
	kfree(hc_dev->c);
out:
	#if (LOCK_USE==0)
	up(&hc_dev->sema);
	#endif
	#if (LOCK_USE==1)
	mutex_unlock(&hc_dev->mtx);
	#endif
	return retval;	 //不能返回0,否则会不停的写
}

int hc_release(struct inode *inode, struct file *filp)
{
	printk(KERN_INFO "%s release\n",current->comm);
	return 0;
}

struct file_operations hc_fops = {		//字符设备的操作函数
	.owner =    THIS_MODULE,
	.read =     hc_read,
	.write =    hc_write,
	.open =     hc_open,
	.release =  hc_release,
};

//1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
static int __init hello_init(void)	
{
	int ret,i;
	printk(KERN_INFO "---BEGIN HELLO LINUX MODULE---\n");
	if(hello_major){
		devt=MKDEV(hello_major,hello_minor);
		ret=register_chrdev_region(devt,hello_nr_devs,"hello_chr");	//使用指定的设备号分配
	}
	else{
		ret = alloc_chrdev_region(&devt,hello_minor,hello_nr_devs,"hello_chr");//动态分配主设备号
		hello_major = MAJOR(devt);
	}
	if (ret < 0) {
		printk(KERN_WARNING "hello: can't get major %d\n", hello_major);
		goto fail;
	}
	
	hc_devp = kzalloc(sizeof(struct hello_char_dev)*hello_nr_devs,GFP_KERNEL);  //给字符设备分配空间,这里hello_nr_devs为2
	if(!hc_devp)
	{
		printk(KERN_WARNING "alloc mem failed");
		ret = -ENOMEM;
		goto failure_kzalloc;	// 内核常用goto处理错误
	}
	
	for(i=0;i<hello_nr_devs;i++){	
	#if (LOCK_USE==0)
		sema_init(&hc_devp[i].sema,1);  // 初始化信号量,第一个参数:信号量地址。第二个参数:信号量个数,这里设为1,1个资源,当一个进程获得,其他进程等待。
	 // 信号量可有多个,有一个进程来获取,信号量个数减1。直到信号量减到0,进程无法获取信号量。当其他进程释放信号量后,被休眠的进程才可以获得信号量。
	#elif (LOCK_USE==1)
		mutex_init(&hc_devp[i].mtx);   // 初始化互斥量, 没有个数问题,因为只有一个,一个进程获得互斥锁,其他进程只能等待。上面信号量初始化为1和这里互斥锁类似。
	#endif 

		cdev_init(&hc_devp[i].cdev,&hc_fops);		// 初始化字符设备结构
		hc_devp[i].cdev.owner = THIS_MODULE;
		ret = cdev_add(&hc_devp[i].cdev,MKDEV(hello_major,hello_minor+i),1);
		if(ret)
		{
			printk(KERN_WARNING"fail add hc_dev%d",i);
		}
	}	
	
	hc_cls = class_create(THIS_MODULE,"hc_dev");
	if(!hc_cls)
	{
		printk(KERN_WARNING"fail create class");
		ret = PTR_ERR(hc_cls);
		goto failure_class;
	}
	for(i=0;i<hello_nr_devs;i++){
		device_create(hc_cls,NULL,MKDEV(hello_major,hello_minor+i),NULL,"hc_dev%d",i);
	}	
	printk(KERN_INFO "---END HELLO LINUX MODULE---\n");
	return 0;

failure_class:
	kfree(hc_devp);
failure_kzalloc:		
	unregister_chrdev_region(devt,hello_nr_devs);
fail:
	return ret;	//返回错误,模块无法正常加载
}

static void __exit hello_exit(void)
{
	int i;	
	for(i=0;i<hello_nr_devs;i++)
	{
		device_destroy(hc_cls,MKDEV(hello_major,hello_minor+i));
	}
	class_destroy(hc_cls);
	for(i=0;i<hello_nr_devs;i++)
		cdev_del(&hc_devp[i].cdev);
	kfree(hc_devp);
	unregister_chrdev_region(devt,hello_nr_devs);	//移除模块时释放设备号		
	printk(KERN_INFO "GOODBYE LINUX\n");
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("Dual BSD/GPL");//许可 GPL、GPL v2、Dual MPL/GPL、Proprietary(专有)等,没有内核会提示
MODULE_AUTHOR("KGZ");		//作者
MODULE_VERSION("V1.0");  	//版本

同理make后insmod,再运行writeloop和echo,dmesg。上面只是在写时加入互斥锁和信号量,读时也要加入。如下红线上面是writeloop,红线下面是bash,如下打印的get sema和up sema之间只有自己的1,2,3步。
在这里插入图片描述

3.实现设备驱动的ioctl函数: nr=number

3.1 hello_chr_locked.h:ioctl的第二个参数即cmd即HC_IOC_RESET,正常情况要不都按值传递,要不都用指针

#ifndef _HELLO_CHR_LOCKED_H_
#define _HELLO_CHR_LOCKED_H_

#define HC_IOC_MAGIC 0x81  //type(0x81未被使用) //内核源码里Documentation/userspace-api/ioctl/ioctl-number.rst
#define HC_IOC_RESET		_IO(HC_IOC_MAGIC,0)  //序号0,作用是清空我们分配的空间,第一个参数幻数
#define HC_IOCP_GET_LENS	_IOR(HC_IOC_MAGIC,1,int) //通过ioctl函数第三个参数以指针方式返回字符串长度
#define HC_IOCV_GET_LENS	_IO(HC_IOC_MAGIC,2)	 //通过ioctl函数返回值返回字符串长度
#define HC_IOCP_SET_LENS 	_IOW(HC_IOC_MAGIC,3,int) //通过ioctl函数第三个参数以指针方式来设置字符串长度
#define HC_IOCV_SET_LENS	_IO(HC_IOC_MAGIC,4)	 //通过ioctl函数第三个参数直接传入值来设置字符串长度
#define HC_IOC_MAXNR 4  //最大命令编号
#endif

3.2 hello_chr_locked.c:ioctl执行硬件控制,除了读写设备文件外的其他操作,比如锁门、弹出介质、设置波特率、设置比特位等

/*
如下第一个参数是文件描述符指针,第二个参数是ioctl命令,第三个参数是第二个参数对应命令的参数
long (*unlocked_ioctl 函数名) (struct file *filp, unsigned int cmd, unsigned long arg)  

ioctl的第二个参数即cmd(32位整形)构成:direction(方向:指明读写,2bits) ,  size(数据大小,14bits)          type(幻数,唯一标识ioctl命令,8bits) ,  number(序数,命令的编号,8bits) 

宏:_IO(type,nr) _IOR(type,nr,size) _IOW(type,nr,size) _IOWR(type,nr,size) // 为了构造上面cmd命令
	_IOC_DIR(nr) _IOC_TYPE(nr) _IOC_NR(nr) _IOC_SIZE(nr)   // 从cmd命令中提取对应字段

函数:access_ok()	//检查用户空间地址是否可用,数据传输之前要用这个检查下
	 put_user()和__put_user()	//向用户空间写数据,put_user安全即里面执行了access_ok()
	 get_user()和__get_user()	//从用户空间接收数据,__get_user需先手动调access_ok()
     capable()	//检查进程是否有权限,因为ioctl要对硬件进行控制和更改,需要进程被授权操作权限

	对于需要传入的数据量不大的ioctl函数,可以使用put_user()或get_user()【单个数据:可能1,2,4,8个字节】,如果传入数据量很大的话,可使用之前的copy_from_user(),copy_to_user()
*/
#include "hello_chr_locked.h"  //上面自定义

long hc_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
	struct hello_char_dev *hc_dev = filp->private_data;  //得到设备的地址
	long retval = 0;
	int tmp,err=0;
	
	//如下两行判断传入的命令是否合法
	if (_IOC_TYPE(cmd) != HC_IOC_MAGIC) return -ENOTTY;	//检查幻数(返回值POSIX标准规定,也用-EINVAL)
	if (_IOC_NR(cmd) > HC_IOC_MAXNR) return -ENOTTY;   //检查命令编号

	//如下只判断读的空间,读的时候使用带下划线函数,写使用安全函数
	if (_IOC_DIR(cmd) & _IOC_READ)	 //涉及到用户空间与内核空间数据交互,判断读OK吗?
		err = !access_ok((void __user *)arg, _IOC_SIZE(cmd)); //第一个参数:用户空间地址。第二个参数:传入参数大小。ok返回1,err就是0,下行if(0)不执行
	if (err) return -EFAULT; 

	// 前面判断都正常,说明命令合法进行如下
	switch(cmd){
		case HC_IOC_RESET:
			printk(KERN_INFO "ioctl reset\n");
			kfree(hc_dev->c);
			hc_dev->n=0;
			break;
		case HC_IOCP_GET_LENS:
			printk(KERN_INFO "ioctl get lens through pointer\n"); //指针方式传递
			retval = __put_user(hc_dev->n,(int __user *)arg); //第一个参数:你要传的数据 //第二个参数:用户空间地址
			break;
		case HC_IOCV_GET_LENS:
			printk(KERN_INFO "ioctl get lens through value\n");
			return hc_dev->n;  //直接return这个值就行
			break;
		case HC_IOCP_SET_LENS:
			printk(KERN_INFO "ioctl set lens through pointer");
			if (! capable (CAP_SYS_ADMIN))   //具有管理员权限才可修改
				return -EPERM;			
			retval = get_user(tmp,(int __user *)arg); //第一个参数:要保存数据的位置,第二个参数:用户空间传入的数据(这里是指针传入)
			//hc_dev->n = min(hc_dev->n,tmp);
			if(hc_dev->n>tmp) //判断这个值,和具体业务实现有关
				hc_dev->n=tmp;
			printk(KERN_INFO " %d\n",hc_dev->n);
			break;
		case HC_IOCV_SET_LENS:
			printk(KERN_INFO "ioctl set lens through value");
			if (! capable (CAP_SYS_ADMIN))
				return -EPERM;			
			hc_dev->n = min(hc_dev->n,(int)arg); //arg就是值,直接赋值
			printk(KERN_INFO " %d\n",hc_dev->n);
			break;
		default:   //前面做了cmd的检查,这里可以不需要
			break;
	}	
	return retval;
}

struct file_operations hc_fops = {		//字符设备的操作函数
	.owner =    THIS_MODULE,
	.read =     hc_read,
	.write =    hc_write,
	.open =     hc_open,
	.release =  hc_release,
	.unlocked_ioctl = hc_ioctl,		//还有一个compat_ioctl(用于32位程序运行于64位系统上)
};

3.3 ioctltest.c:测试函数操作ioctl,先insmod hello_chr_locked.ko

#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
#include<sys/ioctl.h>   //包括了一些构造命令那些宏
#include<errno.h>
#include"hello_chr_locked.h"   //这样才能在应用空间找到对应命令

int main(int argc ,char* argv[])
{
	int n,retval=0;
	int fd;
	fd = open("/dev/hc_dev0",O_RDWR);
	switch(argv[1][0])
	{
		case '0':   //复位
			ioctl(fd,HC_IOC_RESET);
	    	printf("reset hc\n");
			break;
		case '1':   //通过指针获取长度
			ioctl(fd,HC_IOCP_GET_LENS,&n);
	    	printf("get lens pointer, %d\n",n);
			break;
		case '2':  //通过值获取长度
			n = ioctl(fd,HC_IOCV_GET_LENS);
	    	printf("get lens value, %d\n",n);
			break;
		case '3':  //通过指针设置长度
			n=argv[2][0]-'0';
			retval = ioctl(fd,HC_IOCP_SET_LENS,&n);
	    	printf("set lens value, %d %s\n",n,strerror(errno));
	 		break;
		case '4':  //通过值设置长度
			n=argv[2][0]-'0';
			retval = ioctl(fd,HC_IOCV_SET_LENS,n);
	    	printf("set lens value, %d %s\n",n,strerror(errno));
	 		break;
	}
	close(fd);
	return 0;
}

在这里插入图片描述

4.实现进程休眠:条件不够歇一歇,把CPU让给其他进程

有时候进程在读设备时,发现设备数据还没准备好,没办法正常读取设备。或在写设备时,发现设备缓冲区满,没办法正常写设备。在遇到这些情况时,进程该何去何从?进程在操作设备时,如果条件不满足,就让它进入休眠等待,直到条件满足,就可唤醒进程进行后面操作。

/*
初始化:
	DECLARE_WAIT_QUEUE_HEAD()   //初始化等待队列头,宏的静态方式
	或
	wait_queue_head_t wq;     //动态方式
	init_waitqueue_head(&wq);
休眠:	
	wait_event()   //不可被打断,死等,直到满足条件为止
	wait_event_interruptible()  //可使用信号打断它,常用
唤醒:	
	wake_up()   //对应wait_event()
	wake_up_interruptible()	 //对应上面可中断方式休眠的进程wait_event_interruptible() 
*/
# include <linux/wait.h>
DECLARE_WAIT_QUEUE_HEAD(wq);  //等待队列头

hc_read函数中
{
	...
	printk(KERN_INFO "read hc_dev %p\n",hc_dev);
 // wait_event(wq,hc_dev->c!=NULL);  //第一个参数:等待队列头。第二个参数:等待条件: 如果设备字符串区(即echo值进设备文件里)为空就进入等待(唤醒方法在write函数中)。
	wait_event_interruptible(wq,hc_dev->c!=NULL);
	...
}

hc_write函数中
{
	...
	printk(KERN_INFO"%s write done",current->comm);
//	wake_up(&wq);  // 当写入字符设备成功后(如上行)就调用wakeup函数唤醒等待队列上的进程
	wake_up_interruptible(&wq);
	return count;	 
	...
}

在这里插入图片描述
如下等待条件满足,如上卡住的即休眠的进程会正常退出(两个进程cat卡住休眠,执行如下一行,两个进程都不会卡住)。
在这里插入图片描述

5.内核表示时间和实现延时:linux中有一个时钟会周期性产生中断,linux将这中断作为时间基准

/*
【中断频率】会保存在HZ(100-1000之间)这个变量里。【中断次数】会保存在jiffies变量里,可通过jiffies值获得到系统从开机到现在的时钟中断次数。常使用HZ和jiffies这两个变量构造各种时间函数

比较2个jiffies时使用下面的宏(可避免32位系统溢出的问题):
time_after(a,b)		a>b?   a>b返回1,否则返回0
time_before(a,b)	a<b?
time_after_eq(a,b)	a>=b?
time_before_eq(a,b)	a<=b?

jiffies与常用时间之间的转换:
jiffies_to_msecs()    //毫秒
jiffies_to_usecs()    //微秒
msecs_to_jiffies()
usecs_to_jiffies() 

jiffies_to_timespec64()  //timespec结构里有两个成员:一个是秒,另一个是微妙
timespec64_to_jiffies()

延时:
wait_event_timeout()  //除了关心条件是否成立外,还关心超时是否到时,如果超时时间一到,不管条件是否满足,这两函数都会返回退出,然后执行后面操作
wait_event_interruptible_timeout()

set_current_state()
schedule_timeout()  //单纯实现延时,不需要管条件,使用时需手动执行上一行改变当前进程状态

ndelay()   三个函数均是忙等待即cpu死循环一直占用cpu资源  //纳秒
udelay()   系统常用, 微秒
mdelay()   毫秒,时间长,如果用到了mdelay,那么你可能需要考虑使用msleep

休眠延时:
usleep_range()   10us以上 20ms以下  
msleep()	毫秒延时
msleep_interruptible()  可用信号打断延时
ssleep()    秒级延时
fsleep(unsigned long usecs) 微妙级以上延迟,底层实现根据括号里提供的延迟长度选择上面ndelay等,5.13.2内核才有
*/
#include<linux/module.h>
#include<linux/jiffies.h>
#include<linux/sched.h>
#include<linux/delay.h>
unsigned long j,t1,diff;
struct timespec64 ts64;

static int __init hello_init(void)	
{
	wait_queue_head_t wait;
	init_waitqueue_head(&wait);

	printk(KERN_INFO "HELLO LINUX MODULE\n");
	j=jiffies; //保存程序刚开始运行的jiffies值
	t1=j+msecs_to_jiffies(1000);  //+1秒(1000毫秒)延时
	printk(KERN_INFO "j=%ld t1=%ld\n af:%d bf:%d afeq:%d beeq:%d\n",j,t1,time_after(j,t1),time_before(j,t1),time_after_eq(j,t1),time_before_eq(j,t1));

	//111111111111111111111111111111111111111111111111111111111如下都是延迟1秒	
	//忙等待,不推荐使用,浪费系统性能,有可能让系统进入死循环出不来(比如禁止了中断)
	printk(KERN_INFO "忙等待延时1s\n");
	while(time_before(jiffies,t1))  //如果jiffies<t1,永远都是真,while一直循环
	{ //当jiffies>t1,条件为假,退出循环,实现了1s延迟
	}
	
	//等待队列延时
	printk(KERN_INFO "等待队列延时1s\n");
	wait_event_interruptible_timeout(wait,0,msecs_to_jiffies(1000));  //休眠函数实现延迟,第二个参数为0是不需要条件,第三个参数是以jiffies表示的时间长度,所以用到时间转为jiffies函数即msecs_to_jiffies即延迟1秒

	//schedule_timeout延时
	printk(KERN_INFO "schedule_timeout延时1s\n");
	set_current_state(TASK_INTERRUPTIBLE); //将进程状态设置为可中断的休眠
	schedule_timeout(msecs_to_jiffies(1000));
	
	//短延时函数
	printk(KERN_INFO "短延时\n");
	mdelay(1000); //延迟1s

	//休眠延时
	printk(KERN_INFO "usleep_range延时\n");
	usleep_range(10000,15000); //10毫秒到15毫秒
	printk(KERN_INFO "ssleep延时\n");
	ssleep(1); //延迟1s

	//1111111111111111111111111111111111111111111111111111111111111111111111111
	diff = jiffies - j;  //获取如上中间一段的延时差值
	//如下在将jiffies转时间就能获取上面代码运行时间
	printk(KERN_INFO"diff=%ld,time ms=%d us=%d\n",diff,jiffies_to_msecs(diff),jiffies_to_usecs(diff)); //将jiffies差值转为毫秒和微妙值	
	printk(KERN_INFO "系统开机到现在的时间:\n");
	jiffies_to_timespec64(jiffies-INITIAL_JIFFIES,&ts64); //INITIAL_JIFFIES: 这个宏的值是系统刚开始运行时系统赋给jiffies的初值
	printk(KERN_INFO"sec:%lld+ns:%ld\n",ts64.tv_sec,ts64.tv_nsec);
	return 0;
}

static void __exit hello_exit(void)
{
	printk(KERN_INFO "GOODBYE LINUX\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("Dual BSD/GPL");//许可 GPL、GPL v2、Dual MPL/GPL、Proprietary(专有)等,没有内核会提示
MODULE_AUTHOR("KGZ");		//作者
MODULE_VERSION("V1.0");  	//版本

在这里插入图片描述

6.内核实现延缓操作:内核定时器,tasklet,workqueue

有时希望一个事件在指定时间之后(或一个事件发生后)运行而不影响当前操作,不影响当前操作,这时需要内核为我们提供一些机制:
1.如果想实现延迟指定时间之后执行指定操作,可使用内核定时器workqueue工作队列
2.如果想实现在一个事件发生之后再去执行其他操作,可使用taskletworkqueue工作队列:这种情况多数发生在中断处理上,当一个中断发生后,在中断回调函数中希望尽可能短和尽可能快执行,所以把那些费时间的,不是很紧要的任务放稍后执行即放在中断回调函数执行。

/*
1、内核定时器
struct timer_list
timer_setup()  //初始化上面结构并绑定回调函数
mod_timer()   //修改延时时间,调用mod_timer()后定时器开始定时,到达指定时间后调用绑定的回调函数
del_timer()  //删除定时器

2、tasklet  
struct tasklet_struct   
tasklet_init()   //初始化:绑定回调函数以及传入参数数据
tasklet_hi_schedule()   //让tasklet执行,带hi的对应回调函数在高优先级执行
tasklet_schedule()   //正常执行tasklet
tasklet_kill()  //移除tasklet  

3、workqueue
alloc_workqueue()   //分配一个工作队列,如果任务不重,可使用内核自带的工作队列
destroy_workqueue()  

struct work_struct  //不需要延迟的操作,声明结构体
INIT_WORK()  //初始化工作
queue_work()  //将工作插入到工作队列中,内核自动调用对应工作

struct delayed_work  //需要延时的工作,先声明结构
INIT_DELAYED_WORK()  //初始化这结构
queue_delayed_work()   //将对应工作插入到工作队列中,内核会在指定定时时间后调用对应工作

4、其他
in_interrupt()   //指明当前上下文是不是在中断上下文,是在中断上下文则返回非0
smp_processor_id()  //返回当前运行所在cpu的id号

通过读取设备文件构造下面的数据表:
// 第一列是用jiffies表示的时间值。delta是两次获取之间的时间间隔。inirq是in_interrupt函数返回值,是否是在中断中。pid表明当前进程执行的pid。cpu是当前进程执行的cpu。command是当前进程的名称。
  time    delta   inirq    pid   cpu   command   
4295601162    0     0      7559   0    cat
4295601162    0     1      10     0   ksoftirqd/0
4295601162    0     1      10     0   ksoftirqd/0
4295601162    0     1      10     0   ksoftirqd/0
4295601162    0     1      10     0   ksoftirqd/0
4295601162    0     1      10     0   ksoftirqd/0
./Documentation/core-api/workqueue.rst
*/
#include<linux/module.h>
#include<linux/fs.h>
#include<linux/cdev.h>
#include<linux/slab.h>
#include<linux/uaccess.h>
#include<linux/interrupt.h>  //包括了tasklet、timer.h和workqueue.h
#include<linux/timer.h>		//定时器
#include<linux/workqueue.h>	//工作队列

#define DEFER_TEST 2  //(0:timer 1:tasklet 2:workqueue)
#define DELAY_WORK   //workqueue通过定义这行宏决定延时还是不延时
#define HELLO_MAJOR 0
#define HELLO_NR_DEVS 2

int hello_major = HELLO_MAJOR;
int hello_minor = 0;
dev_t devt;      //高12位是主设备号,低20位是次设备号
int hello_nr_devs = HELLO_NR_DEVS;

module_param(hello_major, int, S_IRUGO);
module_param(hello_minor, int, S_IRUGO);
module_param(hello_nr_devs, int, S_IRUGO);

struct hello_char_dev{	//实际的字符设备结构,类似于面向对象的继承
	struct cdev cdev;
	char * buff;  //管理缓冲区
	int loops;  //限制循环次数
	int tdelay;  //两次打印时间间隔
	unsigned long prej;  //保存上一次jiffies时间值
#if (DEFER_TEST==0)  //DEFER_TEST宏管理(在最前面定义)
	struct timer_list t1;  //使用定时器,声明定时器结构
#elif (DEFER_TEST==1)
	struct tasklet_struct tsklt;  //使用tasklet,声明tasklet结构
#else
#ifdef DELAY_WORK  //通过这个宏决定延迟还是不延迟的工作队列
	struct delayed_work work;  //使用workqueue,声明workqueue结构
#else
	struct work_struct work;
#endif
#endif
}; //如上是整个结构体
struct hello_char_dev *hc_devp;
struct class *hc_cls;
DECLARE_WAIT_QUEUE_HEAD(wq);

int hc_open(struct inode *inode, struct file *filp)
{
	struct hello_char_dev *hc_dev;
	printk(KERN_INFO "%s open \n",current->comm);
	hc_dev = container_of(inode->i_cdev,struct hello_char_dev,cdev);  //获取设备结构体的地址
	filp->private_data = hc_dev;	
	return 0;
}

//11111111111111111111111111111111111111111111111111111111111111111111111定时器begain
#if (DEFER_TEST==0)
void timer_fn(struct timer_list *t)  //回调函数,timer_list是放在字符设备的结构体中,所以下行要用container_of通过结构体成员获得结构体指针
{
	struct hello_char_dev *hc_dev =	container_of(t,struct hello_char_dev,t1); //拿到字符设备指针
	hc_dev->buff+=sprintf(hc_dev->buff,"%9ld  %3ld     %i  %6i     %i  %s\n",
			 jiffies, jiffies-hc_dev->prej, in_interrupt()? 1 : 0,
			 current->pid, smp_processor_id(), current->comm);
	if(--hc_dev->loops)  //如果loops值不为0
	{
		mod_timer(t,jiffies+hc_dev->tdelay);  //改变定时器定时时间,继续让定时器延时回调,循环5次
		hc_dev->prej = jiffies;
	}
	else{ // 5次后loops为0执行else
		del_timer(t); //删除定时器
		//del_timer_sync(t);
		wake_up_interruptible(&wq);  //唤醒hc_read函数进程
	}
}

ssize_t hc_read(struct file *filp, char __user *buf, size_t count,loff_t *f_pos)
{
	ssize_t retval=0;
	size_t cnt;
	char *buf1;
	struct hello_char_dev *hc_dev=filp->private_data;  //拿到字符设备指针
	printk(KERN_INFO "timer defer test\n");
	if(*f_pos > 0)
		goto out;
	hc_dev->loops = 5;
	hc_dev->tdelay = 10;   //定时器每隔10
	buf1 = kzalloc(400,GFP_KERNEL);  //给缓冲区分配空间保存在buf1临时变量中
	hc_dev->buff = buf1; //下面几行是在缓冲里放数据
	hc_dev->buff += sprintf(hc_dev->buff, "timer defer test\n");
	hc_dev->buff += sprintf(hc_dev->buff, "   time   delta  inirq    pid   cpu command\n");
	hc_dev->buff += sprintf(hc_dev->buff, "%9li  %3li     %i    %6i   %i   %s\n",
			jiffies, 0L, in_interrupt() ? 1 : 0,
			current->pid, smp_processor_id(), current->comm);  // 赋一行数据
	timer_setup(&hc_dev->t1,timer_fn,0);  //初始化定时器,timer_fn对应回调函数
	mod_timer(&hc_dev->t1,jiffies+hc_dev->tdelay);  //修改delay时间,当前jiffies+需延迟jiffies值
	hc_dev->prej = jiffies; //当前jiffies值保存在字符设备prej变量中

	wait_event_interruptible(wq, !hc_dev->loops);  //让当前的read进行休眠

	//如下是timer_fn回调函数将此函数hc_read唤醒,继续执行
	cnt =hc_dev->buff - buf1;  //多少字节
	if(copy_to_user(buf,buf1,cnt)) //将数据传给用户空间
	{
		retval = -EFAULT;
		goto out;
	}
	*f_pos += cnt;
	kfree(buf1);
	return cnt;	
out:
	return retval;
}  //如上整个操作完成了,实现了定时器每隔10个jiffies回调一次,打印一下当前信息,一共打印5次
//11111111111111111111111111111111111111111111111111111111111111111111定时器end

//1111111111111111111111111111111111111111111111111111111111111111111tasklet begain
#elif (DEFER_TEST==1)
void tasklet_fn(unsigned long data)  //回调函数
{
	struct hello_char_dev *hc_dev =(struct hello_char_dev *) data;
	hc_dev->buff+=sprintf(hc_dev->buff,"%9ld  %3ld     %i  %6i     %i  %s\n",
			 jiffies, jiffies-hc_dev->prej, in_interrupt()? 1 : 0,
			 current->pid, smp_processor_id(), current->comm);
	if (--hc_dev->loops) {
		hc_dev->prej = jiffies;
		tasklet_hi_schedule(&hc_dev->tsklt);
		//tasklet_schedule(&hc_dev->tsklt);
	} else { // 5次后,loops为0
		wake_up_interruptible(&wq);  //唤醒读进程
	}
}

ssize_t hc_read(struct file *filp, char __user *buf, size_t count,loff_t *f_pos)
{
	ssize_t retval=0;
	int cnt;
	char * buf1;
	struct hello_char_dev *hc_dev=filp->private_data;
	printk(KERN_INFO "tasklet defer test\n");
	if(*f_pos > 0)
		goto out;
	hc_dev->loops = 5;
	buf1 = kzalloc(400,GFP_KERNEL);
	hc_dev->buff = buf1;
	hc_dev->buff += sprintf(hc_dev->buff, "tasklet defer test\n");
	hc_dev->buff += sprintf(hc_dev->buff, "   time   delta  inirq    pid   cpu command\n");
	hc_dev->buff += sprintf(hc_dev->buff, "%9li  %3li     %i    %6i   %i   %s\n",
			jiffies, 0L, in_interrupt() ? 1 : 0,
			current->pid, smp_processor_id(), current->comm);
	tasklet_init(&hc_dev->tsklt,tasklet_fn,(unsigned long)hc_dev); //第一个参数:tasklet结构体地址。第二个参数:回调函数。第三个参数:传入回调函数的数据参数,将字符设备地址强转为long传入。
	hc_dev->prej = jiffies;
	tasklet_hi_schedule(&hc_dev->tsklt);
	//tasklet_schedule(&hc_dev->tsklt);

	wait_event_interruptible(wq, !hc_dev->loops);  //等待执行完成
	tasklet_kill(&hc_dev->tsklt); //清除tasklet
	cnt =hc_dev->buff - buf1;  //获得到这个buf里有多少字节
	if(copy_to_user(buf,buf1,cnt))  //将数据传给用户空间
	{
		retval = -EFAULT;
		goto out;
	}
	*f_pos += cnt;
	kfree(buf1);  //释放存储空间返回
	return cnt;	
out:
	return retval;
}
//111111111111111111111111111111111111111111111111111111111111111111111tasklet end

//1111111111111111111111111111111111111111111111111111111111111111111workqueue begain
#else
struct workqueue_struct *workq = NULL;

void workqueue_fn(struct work_struct *work) //回调函数
{
#ifdef DELAY_WORK
	struct delayed_work *dwork = container_of(work,struct delayed_work,work); //通过work找到delayed_work地址
	struct hello_char_dev *hc_dev =	container_of(dwork,struct hello_char_dev,work); //通过dwork地址找到字符设备地址
#else
	struct hello_char_dev *hc_dev =	container_of(work,struct hello_char_dev,work);
#endif
	hc_dev->buff+=sprintf(hc_dev->buff,"%9ld  %3ld     %i  %6i     %i  %s\n",
			 jiffies, jiffies-hc_dev->prej, in_interrupt()? 1 : 0,
			 current->pid, smp_processor_id(), current->comm);
	if (--hc_dev->loops) {
		hc_dev->prej = jiffies;
#ifdef DELAY_WORK
		queue_delayed_work(workq,dwork,hc_dev->tdelay); //继续执行回调函数
#else
		queue_work(workq,work);
#endif
	} else {
		wake_up_interruptible(&wq);
	}
}

ssize_t hc_read(struct file *filp, char __user *buf, size_t count,loff_t *f_pos)
{
	ssize_t retval=0;
	int cnt;
	char * buf1;
	struct hello_char_dev *hc_dev=filp->private_data;
	printk(KERN_INFO "workqueue defer test\n");
	if(*f_pos > 0)
		goto out;
	workq = alloc_workqueue("workq",WQ_UNBOUND,0); // 第三个参数默认填0
	hc_dev->loops = 5;
#ifdef DELAY_WORK
	hc_dev->tdelay = 10;
#endif
	buf1 = kzalloc(400,GFP_KERNEL);
	hc_dev->buff = buf1;
	hc_dev->buff += sprintf(hc_dev->buff, "workqueue defer test\n");
	hc_dev->buff += sprintf(hc_dev->buff, "   time   delta  inirq    pid   cpu command\n");
	hc_dev->buff += sprintf(hc_dev->buff, "%9li  %3li     %i    %6i   %i   %s\n",
			jiffies, 0L, in_interrupt() ? 1 : 0,
			current->pid, smp_processor_id(), current->comm);	
	hc_dev->prej = jiffies;	
#ifdef DELAY_WORK
	INIT_DELAYED_WORK(&hc_dev->work,workqueue_fn);  //初始化延迟工作队列
	queue_delayed_work(workq,&hc_dev->work,hc_dev->tdelay); //将工作添加到工作队列中,第三个参数指定延时时间
#else
	INIT_WORK(&hc_dev->work,workqueue_fn);
	queue_work(workq,&hc_dev->work);
#endif	
	wait_event_interruptible(wq, !hc_dev->loops); // 进程休眠
	destroy_workqueue(workq);
	cnt =hc_dev->buff - buf1;
	if(copy_to_user(buf,buf1,cnt))
	{
		retval = -EFAULT;
		goto out;
	}
	*f_pos += cnt;
	kfree(buf1);
	return cnt;	
out:
	return retval;
}	
#endif
//1111111111111111111111111111111111111111111111111111111111111111workqueue end

ssize_t hc_write(struct file *filp, const char __user *buf, size_t count,loff_t *f_pos)
{
	printk(KERN_INFO "write hc_dev\n");
	return count;					//不能返回0,否则会不停的写
}

int hc_release(struct inode *inode, struct file *filp)
{
	printk(KERN_INFO "release hc_dev\n");
	return 0;
}

struct file_operations hc_fops = {		//字符设备的操作函数
	.owner =    THIS_MODULE,
	.read =     hc_read,
	.write =    hc_write,
	.open =     hc_open,
	.release =  hc_release,
};

static int __init hello_init(void)	
{
	int ret,i;
	printk(KERN_INFO "---BEGIN HELLO LINUX MODULE---\n");
	if(hello_major){
		devt=MKDEV(hello_major,hello_minor);
		ret=register_chrdev_region(devt,hello_nr_devs,"hello_chr");	//使用指定的设备号分配
	}
	else{
		ret = alloc_chrdev_region(&devt,hello_minor,hello_nr_devs,"hello_chr");//动态分配主设备号
		hello_major = MAJOR(devt);
	}
	if (ret < 0) {
		printk(KERN_WARNING "hello: can't get major %d\n", hello_major);
		goto fail;
	}
	
	hc_devp = kzalloc(sizeof(struct hello_char_dev)*hello_nr_devs,GFP_KERNEL);  //给字符设备分配空间,这里hello_nr_devs为2
	if(!hc_devp)
	{
		printk(KERN_WARNING "alloc mem failed");
		ret = -ENOMEM;
		goto failure_kzalloc;		//内核常用goto处理错误
	}
	
	for(i=0;i<hello_nr_devs;i++){	
		cdev_init(&hc_devp[i].cdev,&hc_fops);		//初始化字符设备结构
		hc_devp[i].cdev.owner = THIS_MODULE;
		ret = cdev_add(&hc_devp[i].cdev,MKDEV(hello_major,hello_minor+i),1);  //添加该字符设备到系统中
		if(ret)
		{
			printk(KERN_WARNING"fail add hc_dev%d",i);
		}
	}	
	
	hc_cls = class_create(THIS_MODULE,"hc_dev");
	if(!hc_cls)
	{
		printk(KERN_WARNING"fail create class");
		ret = PTR_ERR(hc_cls);
		goto failure_class;
	}
	for(i=0;i<hello_nr_devs;i++){
		device_create(hc_cls,NULL,MKDEV(hello_major,hello_minor+i),NULL,"hc_dev%d",i);
	}	
	printk(KERN_INFO "---END HELLO LINUX MODULE---\n");
	return 0;

failure_class:
	kfree(hc_devp);
failure_kzalloc:		
	unregister_chrdev_region(devt,hello_nr_devs);
fail:
	return ret;	//返回错误,模块无法正常加载
}

static void __exit hello_exit(void)
{
	int i;	
	for(i=0;i<hello_nr_devs;i++)
	{
		device_destroy(hc_cls,MKDEV(hello_major,hello_minor+i));
	}
	class_destroy(hc_cls);
	for(i=0;i<hello_nr_devs;i++)
		cdev_del(&hc_devp[i].cdev);
	kfree(hc_devp);
	unregister_chrdev_region(devt,hello_nr_devs);	//移除模块时释放设备号		
	printk(KERN_INFO "GOODBYE LINUX\n");
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("Dual BSD/GPL");//许可 GPL、GPL v2、Dual MPL/GPL、Proprietary(专有)等,没有内核会提示
MODULE_AUTHOR("KGZ");		//作者
MODULE_VERSION("V1.0");  	//版本

在这里插入图片描述
如下delta连1次jiffies值改变都没达到。
在这里插入图片描述
非延迟工作队列效果跟tasklet类似,把API换成没有delayed的即可。
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/595512.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

sqlalchemy 分表实现方案

1.需求及场景概述 现有系统中因历史数据量过大,产生了将历史数据进行按月存储的要求,系统和数据库交互使用的是sqlalchemy,假设系统的原来的历史记录表(record)如下: 为了将历史数据按月分表存储,我们需要以此表为基础按月创建对应的月表来进行分表存储,同时又要使用or…

学华为沟通,汇总5大项目沟通技巧

高效沟通在项目管理中的重要性不容小觑&#xff0c;它是确保项目顺利进行、提升团队协作效率、实现项目目标的关键因素。如果沟通不畅&#xff0c;往往容易导致成员对项目目标理解不一致&#xff0c;或信息传递不及时不准确&#xff0c;导致项目工作方向偏差&#xff0c;增加项…

前端工程化05-初始前端工程化Node基本介绍安装配置基础知识

6、初始前端工程化 6.1、工程化概述 虽然前几篇我的目录标题写的前端工程化&#xff0c;但是那些东西并不属于前端工程化的内容&#xff0c;更倾向于是js、jq当中的东西&#xff0c;下面我们将接触真正的前端工程化。 前端工程化开发其实现在是离不开一个东西的&#xff0c;…

Matlab 手写板设计

1、介绍 MATLAB手写板可以作为一个很好的数据输入口&#xff0c;其可以获取该手写板上任意字母、数字&#xff0c;甚至可以制作样本数据。具体用途体现在如下几方面&#xff1a; 数学公式输入&#xff1a;手写板允许用户直接用手写方式输入复杂的数学公式&#xff0c;这对于使…

电子书制作神器,简单操作

​随着数字化时代的到来&#xff0c;电子书籍越来越受到人们的喜爱。而一款优秀的电子翻页书制作软件&#xff0c;则能够帮助你轻松制作出专业级的电子书&#xff0c;让你的阅读体验更加丰富多彩。 今天&#xff0c;我们就来为大家推荐一款优秀的电子翻页书制作软件——FLBOOK在…

Burp和Proxifier抓包微信小程序

1、Burp设置代理 2、浏览器下载证书 3、安装证书 4、Proxifier设置代理 5、Proxifier设置Proxification Rule 6、Burp查看抓包数据 打开一个小程序&#xff0c;可以看到WeChatAppEx的流量先经过Proxifier&#xff0c;再经过127.0.0.1:8080到Burp

如何使用ArcGIS Pro进行选房分析

无论是研究城市规划布局还是寻找理想的住房&#xff0c;都需要综合考虑购物、医疗、教育和休闲等多方面因素&#xff0c;此时我们的GIS软件就可以派上用场了&#xff0c;这里为大家介绍一下如何使用 ArcGIS Pro 进行选房分析&#xff0c;希望能对你有所帮助。 数据来源 教程所…

性能优化 | el-table中内嵌大量el-input控件导致渲染卡顿的问题

场景 项目中有一个应用场景&#xff0c;用户需要在表单中大量使用选择框以及输入框填写数据&#xff08;每一行大概有三十几个输入框&#xff09;&#xff0c;当选择框与输入框达到一定数量的时候&#xff0c;页面会出现输入不连续、卡顿的现象&#xff0c;如下图&#xff1a;…

LoRa无线通讯入门

本文图片来自于深入浅出讲解LoRa通信技术&#xff0c;LoRa技术介绍&#xff0c;LoRa开发与应用&#xff0c;物联网学习必备知识点&#xff01;_哔哩哔哩_bilibili LoRa无线通讯 LoRa&#xff08;Long Range&#xff09;是一种低功耗广域网&#xff08;LPWAN&#xff09;技术&a…

【春招特供】Unity面试题总结 | Unity基础篇

物体发生碰撞的必要条件&#xff1f; 两个物体都必须带有碰撞器&#xff08;Collider&#xff09;&#xff0c;其中一个物体还必须带有Rigidbody刚体&#xff0c;而且必须是运动的物体带有Rigidbody脚本才能检测到碰撞。 2. Unity3d中的碰撞器和触发器的区别&#xff1f; 碰…

颠覆传统?「一束光子,两种频率」的量子纠缠!

在最新的研究中&#xff0c;科学家们开发了一种革命性的量子纠缠方式——“频域光子数路纠缠”&#xff08;frequency-domain photon number-path entanglement&#xff09;。这一量子物理学的重大进展涉及到一个创新性的工具&#xff1a;频率分束器&#xff08;frequency beam…

B2985A是德科技B2985A高阻计

181/2461/8938产品概述&#xff1a; B2985A 静电计/高阻表 描述 B2985A 静电计/高阻表是全球少有具有图形显示功能的静电计&#xff0c;可凭借 0.01 fA&#xff08;0.01 x 10-15 A&#xff09;的分辨率帮助您可靠测量弱电流&#xff0c;并可测量高达 10 PΩ&#xff08;10 x 1…

[leetcode] 62. 不同路径

文章目录 题目描述解题方法方法一&#xff1a;动态规划java代码复杂度分析 方法二&#xff1a;排列组合java代码复杂度分析 相似题目 题目描述 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器人每次只能向下或者向右…

毕业设计:《基于 Prometheus 和 ELK 的基础平台监控系统设计与实现》

前言 《基于 Prometheus 和 ELK 的基础平台监控系统设计与实现》&#xff0c;这是我在本科阶段的毕业设计&#xff0c;通过引入 Prometheus 和 ELK 架构实现企业对指标与日志的全方位监控。并且基于云原生&#xff0c;使用容器化持续集成部署的开发方式&#xff0c;通过 Sprin…

开源模型应用落地-CodeQwen模型小试-SQL专家测试(二)

一、前言 代码专家模型是基于人工智能的先进技术&#xff0c;它能够自动分析和理解大量的代码库&#xff0c;并从中学习常见的编码模式和最佳实践。这种模型可以提供准确而高效的代码建议&#xff0c;帮助开发人员在编写代码时避免常见的错误和陷阱。 通过学习代码专家模型&…

使用Bandzip分卷压缩文件

需求 部分文件太大&#xff0c;例如超过10G&#xff0c;就不能使用企业微信等传输&#xff0c;如果可以把一个10G的文件分割成为10个1G的文件就可以方便传输了。 实现方式 使用bandzip&#xff0c;把超过10G的文件分卷压缩成为多个小的zip文件。 把生成的多个文件放在同一目…

网红隋总揭秘:高效实施人力RPO项目的秘诀

随着企业对于招聘流程效率和专业性的追求日益提升&#xff0c;RPO(招聘流程外包)项目逐渐成为人力资源领域的热门话题。隋总凭借丰富的行业经验和独特的视角&#xff0c;分享了关于如何高效实施人力RPO项目的秘诀。 一、明确目标&#xff0c;找准定位 在启动RPO项目之前&#x…

零基础入门篇④ 初识Python(注释、编码规范、关键字...)

Python从入门到精通系列专栏面向零基础以及需要进阶的读者倾心打造,9.9元订阅即可享受付费专栏权益,一个专栏带你吃透Python,专栏分为零基础入门篇、模块篇、网络爬虫篇、Web开发篇、办公自动化篇、数据分析篇…学习不断,持续更新,火热订阅中🔥专栏订阅地址 👉Python从…

【挑战30天首通《谷粒商城》】-【第一天】01、简介-项目介绍

文章目录 课程介绍一、 项目介绍1、项目背景A、电商模式1、B2B 模式2、B2C 模式3、C2B 模式4、C2C 模式5、O2O 模式 1.2、项目架构图1.3、项目技术 & 特色1.4、项目前置要求二、分布式基础概念(略)三、环境撘建(略) one more thing 课程介绍 1.分布式基础(全栈开发篇)2.分…

UE5 audio capture 回声问题 ||在安卓上有爆鸣声

参考视频 0.基本步骤 【UE4_蓝图】录制麦克风声音/系统声音并输出保存WAV文件_ue4录音-CSDN博客 1.步骤 1.创建Sound Submix A 2. 右键新建Sound Submix B 3.把B的两个参数调为-96 4.audio capture的Base Submix&#xff0c;把前面提到的A赋值进去 5.开始录制输出和完成录制…