当前位置:   article > 正文

字符设备驱动开发框架_字符设备驱动框架

字符设备驱动框架

目录

1 框架

1.1 模块加载命令 

1.2 导出声明

1.3 字符设备驱动要素

2 申请 / 销毁设备号

2.1 申请设备号

 2.2 const struct file_operations *fops

2.2.1 结构体中提供了哪些接口

2.2.2 数据传递 

2.3 销毁 / 注销设备号 

 3 创建设备节点

3.1 手动创建

3.2 程序创建(自动创建)(通过udev/mdev机制) 

3.3  销毁设备节点

4 地址映射

 4.1 操作地址

5. 框架总结

6 程序实例

7 按键中断驱动(在设备树中添加)

设备树介绍:

7.1 获取中断号

 7.2 申请中断

7.2.1 int request_irq

7.2.2 工作流程 

7.3 中断处理函数

7.4 释放中断

7.5 程序示例

7.5.1 结构体信息

7.5.2 应用程序接口和函数

7.5.3 入口函数

7.5.4 中断处理函数

7.5.5 应用程序

 8 文件IO模型(阻塞、非阻塞、IO多路复用、异步通知)

8.1 阻塞

8.1.1 等待队列头

 8.1.2 休眠等待

8.1.3 进程唤醒 

8.2 非阻塞

 8.3 异步通知

8.3.1 驱动编写

8.3.2 应用程序编写

8.4 示例代码 

9 中断下半部分

 9.1 中断下半部的两种实现机制

9.1.1 tasklet机制(小任务机制)

9.1.2 workqueue机制(工作队列机制)

9.2 利用workqueue示例代码

1 框架

头文件

驱动模块装载入口和卸载入口声明

模块装载函数和卸载函数

GPL声明

  1. //头文件
  2. #include<linux/init.h>
  3. #include<linux/module.h>
  4. //模块加载入口函数实现
  5. static int __init (函数名1)(void)
  6. {
  7. //资源的创建,申请,创建驱动
  8. return 0;
  9. }
  10. //模块卸载入口函数实现
  11. static void __exit (函数名2)(void)
  12. {
  13. //资源的释放,删除驱动
  14. }
  15. //模块入口声明
  16. //装载声明(内核加载的入口指定)
  17. module_init(函数名1);//只要加载就执行其中声明的函数
  18. //卸载声明(内核卸载的入口指定)
  19. module_exit(函数名2);
  20. //GPL开源声明
  21. MODULE_LICENSE("GPL");

1.1 模块加载命令 

insmod 

功能:加载模块

命令:insmod 模块路径/模块名  例如:insmod /lib/modules/yhw.ko

rmmod

功能:卸载模块

命令:rmmod 模块路径/模块名  

lsmod 

功能:查看已经加载的模块

1.2 导出声明

如果模块中内容需要在其他模块中使用,可以把内容进行导出--->添加导出声明
EXPORT_SYMBOL(内容名字);
在要使用的模块中进行声明
如果模块只用于进行导出,可以不写入口声明以及定义

举例:

  1. //math.c
  2. #include <linux/init.h>
  3. #include <linux/module.h>
  4. int myadd(int a,int b)
  5. {
  6. return a+b;
  7. }
  8. int mysub(int a,int b)
  9. {
  10. return a-b;
  11. }
  12. EXPORT_SYMBOL(myadd);
  13. EXPORT_SYMBOL(mysub);
  14. static int __init math_init(void)
  15. {
  16. return 0;
  17. }
  18. static void __exit math_exit(void)
  19. {
  20. }
  21. module_init(math_init);
  22. module_exit(math_exit);
  23. MODULE_LICENSE("GPL");
  1. //hello.c
  2. #include <linux/init.h>
  3. #include <linux/module.h>
  4. int myadd(int a,int b);
  5. int mysub(int a,int b);
  6. static int __init hello_init(void)
  7. {
  8. //资源的创建,申请,创建驱动
  9. printk("%d\n",myadd(1,2));
  10. return 0;
  11. }
  12. static void __exit hello_exit(void)
  13. {
  14. //资源的释放,删除驱动
  15. printk("%d\n",mysub(1,2));
  16. }
  17. module_init(hello_init);
  18. module_exit(hello_exit);
  19. MODULE_LICENSE("GPL");

 上面写了两个模块,一个是math.c 一个是hello.c  那么我们想要在hello.c模块中使用math.c模块中的内容,就需要在math.c中添加声明:

  1. EXPORT_SYMBOL(myadd);
  2. EXPORT_SYMBOL(mysub);

表示我们要在其它模块使用本模块中的两个函数myadd和mysub,然后在hello.c中定义并传参。这样就实现在其它模块中使用另一个模块中的内容。(导出的内容为函数或全局变量)

但是!在编写驱动时,有些变量是不确定的,是根据驱动具体加载到哪个设备才确定,在进行
insmod装载驱动模块时再传递这些参数值,那么如何处理参数传递呢?

module_param(name,type,perm)

参数1:参数的名字,变量名
参数2:参数的类型,int,char
参数3:/sys/modules 文件的权限,0666

例如:

  1. module_param(a,int,0644);
  2. module_param(b,int,0644);


1.3 字符设备驱动要素

在内核中,有很多的设备驱动,所以需要一个设备号进行区分;

用户程序也必须知道设备驱动对应到哪个设备节点(文件);

2 申请 / 销毁设备号

2.1 申请设备号

在Linux系统下/proc/devices:文件中包含了所有注册到内核的设备,可以打开看一下

int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops)


参数:
参数1:
unsigned int major:主设备号
设备号:32bit == 主设备号(12bit) + 次设备号(20bit)
主设备号:表示同一类型的设备
次设备号:表示同一类型中的不同设备
参数有两种设置:
静态:例如指定一个整数:250----主设备号为250
动态:让内核随机指定,参数为0
参数2:
const char *name:一个字符串,描述设备信息,自定义
参数3:
const struct file_operations *fops:结构体指针,结构体变量的地址(结构体中就是应
用程序和驱动程序函数关联,open、read、write)---文件操作对象,提供open、read、write等驱动中的函数
返回值:
如果是静态指定主设备号,返回0表示申请成功,返回负数表示申请失败
如果是动态申请,返回值就是申请成功的主设备号
 

 2.2 const struct file_operations *fops

在驱动中要去实现文件IO的接口,通过结构体指针,指向一个结构体,这个结构体当中提供了驱动与应用程序文件IO的接口关联。

2.2.1 结构体中提供了哪些接口

  1. struct file_operations {
  2. struct module *owner;
  3. loff_t (*llseek) (struct file *, loff_t, int);
  4. ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
  5. ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
  6. ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long,
  7. loff_t);
  8. ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long,
  9. loff_t);
  10. int (*iterate) (struct file *, struct dir_context *);
  11. unsigned int (*poll) (struct file *, struct poll_table_struct *);
  12. long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
  13. long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
  14. int (*mmap) (struct file *, struct vm_area_struct *);
  15. int (*open) (struct inode *, struct file *);
  16. int (*flush) (struct file *, fl_owner_t id);
  17. int (*release) (struct inode *, struct file *);
  18. int (*fsync) (struct file *, loff_t, loff_t, int datasync);
  19. int (*aio_fsync) (struct kiocb *, int datasync);
  20. int (*fasync) (int, struct file *, int);
  21. int (*lock) (struct file *, int, struct file_lock *);
  22. ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
  23. unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long,
  24. unsigned long, unsigned long);
  25. int (*check_flags)(int);
  26. int (*flock) (struct file *, int, struct file_lock *);
  27. ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *,
  28. size_t, unsigned int);
  29. ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *,
  30. size_t, unsigned int);
  31. int (*setlease)(struct file *, long, struct file_lock **);
  32. long (*fallocate)(struct file *file, int mode, loff_t offset,
  33. loff_t len);
  34. int (*show_fdinfo)(struct seq_file *m, struct file *f);
  35. };
  36. //函数指针集合,其实就是接口,表示文件IO函数用函数指针来表示,存储驱动中的关联函数
  37. // 如: read函数指针 = xxxx_ok;表示 read函数,在驱动中的关联函数为xxxx_ok函数

2.2.2 数据传递 

//这个功能一般用于驱动中的 read
long copy_to_user(void __user *to,const void *from, unsigned long n)

功能:从驱动空间拷贝数据给用户空间

参数:
参数1:
void __user *to:目标地址,应用空间地址
参数2:
const void *from:源地址,内核空间地址
参数3:
unsigned long n:个数
返回值:
成功返回0,失败返回大于0,表示还有多少个没有拷贝完

 long copy_from_user(void * to,const void __user * from,unsigned long n)

功能:从用户空间拷贝数据到驱动空间

参数:
参数1:
void *to:目标地址,内核空间地址
参数2:
const void *from:源地址,应用空间地址
参数3:
unsigned long n:个数
返回值:
成功返回0,失败返回大于0,表示还有多少个没有拷贝完

2.3 销毁 / 注销设备号 

void unregister_chrdev(unsigned int major,const char * name)


参数:
参数1:
unsigned int major:主设备号
参数2:
const char * name:设备信息,自定义

 3 创建设备节点

3.1 手动创建

mknod 设备节点名 设备类型 主设备号 次设备号
#如:
mknod /dev/xxx c 250 0

3.2 程序创建(自动创建)(通过udev/mdev机制) 

//创建一个类信息

struct class * class_create(owner,name)


参数:
参数1:
owner:一般填写 THIS_MODULE
参数2:
name:字符串首地址,名字,自定义
返回值:
返回值就返回信息结构体的地址

//创建设备节点(设备文件)

struct device * device_create(struct class *class, struct device *parent,dev_t devt, void *drvdata, const char *fmt, ...)


参数:
参数1:
struct class *class:class信息对象地址,通过 class_create()函数创建
参数2:
struct device *parent:表示父亲设备,一般填 NULL
参数3:
dev_t devt:设备号(主设备+次设备)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
参数4:
void *drvdata:私有数据,一般填NULL
参数5、参数6:
const char *fmt, ...:可变参数,表示字符设备文件名(设备节点名)
 

3.3  销毁设备节点

void device_destroy(struct class * class,dev_t devt)

4 地址映射

如果我们想要去控制外设,其实就是控制地址,内核驱动中通过虚拟地址操作,外设是实际的物理地址,每个设备出厂时就有地址

为什么要进行地址映射

1)MMU限制:大多数CPU的MMU只能映射大小为4KB的页面大小,而物理内存中的一块区域大小可能大于4KB,此时需要ioremap来映射不连续的页面。

2)ISA对齐要求:某些外设的寄存器访问需要特定的对齐方式,而CPU访问数据总线的宽度可能无法满足,需要借助ioremap来映射满足要求的虚拟地址。

3)虚拟地址空间不足:当直接映射(网络设备描述符等)占用太多的线性地址空间时,可以使用ioremap来释放部分线性地址,从而获得更多的虚拟地址空间。

4)外设物理地址变化:有些外设允许更改其物理基地址,当基地址变化后,原来的直接映射会失效,此时需要调用ioremap再次建立映射关系。

ioremap实现原理: 

利用页表 entries 将不连续的物理页面映射到连续的虚拟地址空间中。它会在页表中设置虚拟地址与物理地址的对应关系,当CPU进行地址翻译时会根据这些关系将虚拟地址转换为物理地址。

 API函数

//虚拟地址映射
void *ioremap(phys_addr_t offset, unsigned long size)
参数:
参数1:
phys_addr_t offset:物理地址
参数2:
unsigned long size:映射大小
返回值:
映射之后的虚拟地址


//结束映射
void iounmap(void *addr)

 4.1 操作地址

完成地址映射后,返回的值为映射后的地址,就可以对地址进行操作,那么如何操作呢?

第一种 通过指针取*的方式,例如volatile unsigned int *gpx1con;*gpx1con进行操作;

第二种,通过radl() writel()

  1. //从地址中读取地址空间的值
  2. u32 readl(const volatile void *addr)
  3. //将value值,存储到对应地址中
  4. void writel(u32 value, volatile void *addr)

5. 框架总结

在模块加载入口中实现:

1) 申请设备号(内核中用于区分和管理不同的字符设备驱动)
2) 创建设备节点(为用户提供一个可操作的文件接口---用户操作文件)
 3)实现硬件初始化
         地址的映射
         实现硬件的寄存器的初始化
         中断的申请
 4)实现文件io接口(struct file_operations fops结构体)

卸载入口实现资源释放 

iounmap();//解除硬件资源映射
device_destroy();//释放设备节点
class_destroy();//释放节点信息类
unregister_chrdev();//释放设备号

同时,还有出错处理,在某个位置出错,要将之前申请的资源释放;

通过面向对象编程思想,用一个结构体来表示一个对象,设计一个类型来描述一个设备的信息,例如: 

  1. struct BEEP
  2. {
  3. unsigned int major;
  4. struct class * cls;
  5. struct device * dev;
  6. unsigned int * pwmtcfg0;
  7. };
  8. struct BEEP beep;//表示一个设备对象

6 程序实例

  1. //beep_drv.c
  2. #include <linux/init.h>
  3. #include <linux/module.h>
  4. #include <linux/fs.h>
  5. #include <linux/device.h>
  6. #include <asm/io.h>
  7. #include <asm/uaccess.h>
  8. //硬件地址宏定义
  9. #define GPD0CON 0x114000A0
  10. #define PWM 0x139D0000
  11. #define PWMTCFG0 0x139D0000
  12. #define PWMTCFG1 0x139D0004
  13. #define PWMTCON 0x139D0008
  14. #define PWMTCNTB0 0x139D000C
  15. #define PWMTCMPB0 0x139D0010
  16. //结构体信息存储设备信息,包括class_create创建类信息、device_create创建设备节点、设备号
  17. struct BEEP
  18. {
  19. unsigned int major;
  20. struct class * cls;
  21. struct device * dev;
  22. unsigned int * pwmtcfg0;
  23. unsigned int * pwmtcfg1;
  24. unsigned int * pwmtcon;
  25. unsigned int * pwmtcntb0;
  26. unsigned int * pwmtcmpb0;
  27. unsigned int * gpd0con;
  28. };
  29. //结构体对象
  30. struct BEEP beep;
  31. 应用程序open调用对应接口函数
  32. int beep_open (struct inode * inode, struct file * filep)
  33. {
  34. *(beep.pwmtcon) |= 1;
  35. return 0;
  36. }
  37. //应用程序release调用对应接口,这些通过结构体
  38. int beep_release (struct inode * inode, struct file * filep)
  39. {
  40. *(beep.pwmtcon) &= ~1;
  41. return 0;
  42. }
  43. //file_operations fops结构体中实现接口
  44. ssize_t beep_write (struct file * filep, const char __user * buf, size_t size, loff_t * fops)
  45. {
  46. unsigned int hz = 0;
  47. copy_from_user(&hz,buf,size);//从用户空间拷贝数据
  48. *(beep.pwmtcntb0) = 100000000 / hz;
  49. *(beep.pwmtcmpb0) = 50000000 / hz;
  50. *(beep.pwmtcon) |= 1<<1;
  51. *(beep.pwmtcon) &= ~(1<<1);
  52. *(beep.pwmtcon) |= 1<<2;
  53. *(beep.pwmtcon) |= 1;
  54. return 0;
  55. }
  56. //文件IO实现操作
  57. const struct file_operations fops = {
  58. .open = beep_open,
  59. .release = beep_release,
  60. .write = beep_write,
  61. };
  62. static int __init beep_init(void)//加载入口
  63. {
  64. //资源申请
  65. int ret;
  66. //申请设备号
  67. beep.major = register_chrdev(0,"beep",&fops);
  68. if( beep.major < 0 )//设备号申请失败
  69. {
  70. printk(KERN_ERR,"register_chrdev error\n");
  71. ret = -ENODEV;
  72. goto err_0;
  73. }
  74. //创建设备节点
  75. beep.cls = class_create(THIS_MODULE,"beep cls");
  76. if(IS_ERR(beep.cls))
  77. {
  78. printk(KERN_ERR,"class_create error\n");
  79. ret = PTR_ERR(beep.cls);
  80. goto err_1;
  81. }
  82. beep.dev = device_create(beep.cls,NULL,MKDEV(beep.major,0),NULL,"beep");
  83. if(IS_ERR(beep.dev))
  84. {
  85. printk(KERN_ERR,"device_create error\n");
  86. PTR_ERR(beep.dev);
  87. goto err_2;
  88. }
  89. //初始化硬件资源
  90. //映射寄存器地址
  91. beep.gpd0con = ioremap(GPD0CON,4);
  92. if(beep.gpd0con == NULL)
  93. {
  94. printk(KERN_ERR,"ioremap error\n");
  95. ret = -ENOMEM;
  96. goto err_3;
  97. }
  98. beep.pwmtcfg0 = ioremap(PWMTCFG0,4);
  99. if(beep.pwmtcfg0 == NULL)
  100. {
  101. printk(KERN_ERR,"ioremap error\n");
  102. ret = -ENOMEM;
  103. goto err_4;
  104. }
  105. beep.pwmtcfg1 = ioremap(PWMTCFG1,4);
  106. if(beep.pwmtcfg1 == NULL)
  107. {
  108. printk(KERN_ERR,"ioremap error\n");
  109. ret = -ENOMEM;
  110. goto err_5;
  111. }
  112. beep.pwmtcon = ioremap(PWMTCON,4);
  113. if(beep.pwmtcon == NULL)
  114. {
  115. printk(KERN_ERR,"ioremap error\n");
  116. ret = -ENOMEM;
  117. goto err_6;
  118. }
  119. beep.pwmtcntb0 = ioremap(PWMTCNTB0,4);
  120. if(beep.pwmtcntb0 == NULL)
  121. {
  122. printk(KERN_ERR,"ioremap error\n");
  123. ret = -ENOMEM;
  124. goto err_7;
  125. }
  126. beep.pwmtcmpb0 = ioremap(PWMTCMPB0,4);
  127. if(beep.pwmtcmpb0 == NULL)
  128. {
  129. printk(KERN_ERR,"ioremap error\n");
  130. ret = -ENOMEM;
  131. goto err_8;
  132. }
  133. //readl和writel操作地址
  134. //*gpd0con = *gpd0con & ~(0XF<<0) | (0X2<<0);
  135. writel(readl(beep.gpd0con) & ~(0XF<<0) | (0X2<<0),beep.gpd0con);
  136. *(beep.pwmtcntb0) = 200000;
  137. *(beep.pwmtcmpb0) = 100000;
  138. *(beep.pwmtcfg0) = *(beep.pwmtcfg0) & ~0xff;
  139. *(beep.pwmtcfg1) &= ~0XF;
  140. *(beep.pwmtcon) &= ~(1<<4);
  141. *(beep.pwmtcon) |= 1<<3;
  142. *(beep.pwmtcon) |= 1<<1;
  143. *(beep.pwmtcon) &= ~(1<<1);
  144. *(beep.pwmtcon) |= 1<<2;
  145. return 0;
  146. //出错处理
  147. err_8:
  148. iounmap(beep.pwmtcntb0);
  149. err_7:
  150. iounmap(beep.pwmtcon);
  151. err_6:
  152. iounmap(beep.pwmtcfg1);
  153. err_5:
  154. iounmap(beep.pwmtcfg0);
  155. err_4:
  156. iounmap(beep.gpd0con);
  157. err_3:
  158. device_destroy(beep.cls,MKDEV(beep.major,0));
  159. err_2:
  160. class_destroy(beep.cls);
  161. err_1:
  162. unregister_chrdev(beep.major,"beep");
  163. err_0:
  164. return ret;
  165. }
  166. static void __exit beep_exit(void)//卸载入口
  167. {
  168. //资源释放
  169. //iounmap();
  170. iounmap(beep.pwmtcmpb0);
  171. iounmap(beep.pwmtcntb0);
  172. iounmap(beep.pwmtcon);
  173. iounmap(beep.pwmtcfg1);
  174. iounmap(beep.pwmtcfg0);
  175. iounmap(beep.gpd0con);
  176. device_destroy(beep.cls,MKDEV(beep.major,0));
  177. class_destroy(beep.cls);
  178. unregister_chrdev(beep.major,"beep");
  179. }
  180. module_init(beep_init);
  181. module_exit(beep_exit);
  182. MODULE_LICENSE("GPL");

下面是应用程序:

  1. #include <sys/types.h>
  2. #include <sys/stat.h>
  3. #include <fcntl.h>
  4. #include <unistd.h>
  5. #include <stdio.h>
  6. int main()
  7. {
  8. int fd = open("/dev/beep",O_RDWR);
  9. int value = 0;
  10. while(1)
  11. {
  12. scanf("%d",&value);
  13. if(value == 0)
  14. break;
  15. write(fd,&value,4);
  16. }
  17. close(fd);
  18. return 0;
  19. }

下面是Makefile文件:

  1. KERNEL_PATH = /home/yhw/Mine/linux-3.14-fs4412 //内核文件路径
  2. CORSS_COMPILE = /opt/arm_none_gcc/gcc-4.6.4/bin/arm-none-linux-gnueabi- //指定交叉编译工具链
  3. CC = $(CORSS_COMPILE)gcc
  4. obj-m += beep_drv.o //驱动
  5. APP_NAME = beep_test //应用程序
  6. all:
  7. make modules -C $(KERNEL_PATH) M=$(shell pwd)
  8. $(CC) $(APP_NAME).c -o $(APP_NAME)
  9. install:
  10. cp *.ko $(APP_NAME) /home/yhw/Mine/NFS/nfshome/rootfs //拷贝到挂载的nfs目录
  11. clean:
  12. make clean -C $(KERNEL_PATH) M=$(shell pwd)
  13. rm $(APP_NAME)

 通过insmod 加载模块,即可实现蜂鸣器的驱动实现,同理,led也可以这样写。

7 按键中断驱动(在设备树中添加)

设备树位置:/arch/arm/boot/dts

设备树介绍:

Linux驱动——设备树_icy、泡芙的博客-CSDN博客

  1. //在设备树中添加一个按键设备信息,添加上使用的中断号,启动内核时i,内核中就会有按键信息(中断号)
  2. key_int_node {
  3. compatible = "key3";
  4. interrupt-parent = <&gpx1>;
  5. interrupts = <2 4>;
  6. };

7.1 获取中断号

在设备中完成信息添加后,在驱动编写中第一步要做的就是获取到中断号

//根据路径获取设备树中的节点
struct device_node *of_find_node_by_path(const char *path);
参数:
参数1:
const char *path:设备树中节点路径
返回值:获取到的节点


//根据节点获取到节点的中断号
unsigned int irq_of_parse_and_map(struct device_node *dev,int index)
参数:
参数1:
struct device_node *dev:设备节点
参数2:
int index:获取节点中第几个中断号
返回值:获取到的中断号

 7.2 申请中断

7.2.1 int request_irq

函数原型:

int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,const char *name, void *dev)

功能:对应中断号出现对应的触发方式,就调用申请中指定的函数进行处理

参数:

        参数1:

                unsigned int irq:设备对应的中断号
        参数2:

                irq_handler_t handler:中断的处理函数
                        typedef irqreturn_t (*irq_handler_t)(int, void *);

         参数3:

                unsigned long flags:中断的触发方式

#define IRQF_TRIGGER_NONE
#define IRQF_TRIGGER_RISING
#define IRQF_TRIGGER_FALLING
#define IRQF_TRIGGER_HIGH
#define IRQF_TRIGGER_LOW
0x00000000
0x00000001
0x00000002
0x00000004
0x00000008

        参数4:

                const char *name  :中断的描述

        参数5:

                void *dev    传入设备结构体指针,用于记录和调试

返回值:

        成功返回0,失败返回负值错误码

7.2.2 工作流程 

1. 检查中断是否已经申请:如果已经申请,直接返回错误。

2. 建立中断描述符:为该中断号创建中断描述符irq_desc,保存相关的处理函数、触发类型、设备信息等。

3. 注册中断:向中断控制器注册该中断服务程序,通常会指定触发条件、优先级等信息。并使能相应的中断。当中断触发后,CPU会调用注册的中断服务程序,通过irq_desc找到相关的处理函数来服务中断。

7.3 中断处理函数

typedef irqreturn_t (*irq_handler_t)(int, void *);

该函数有两个参数,int 和void *;

int :相应的中断号

void *:需要与request_irq函数的参数dev保持一致,用于区分共享中断的不同设备(dev也可以指向设备数据结构)

返回值:

IRQ_HANDLED:表示中断已被正常处理。内核会继续处理中断返回后的操作。

IRQ_NONE:表示中断未被处理。内核将该中断标记为factual,并尝试交由其他注册的中断处理程序处理,或采取默认处理方法。

一般我们的中断处理程序处理了中断请求后应返回IRQ_HANDLED。只有在确认该中断不是我们服务的中断时才返回IRQ_NONE。 

7.4 释放中断

void free_irq(unsigned int irq,void * dev_id);

参数:

irq:要释放的中断号

dev_id: request_irq时传入的dev参数,用于匹配确认正确的中断

7.5 程序示例

7.5.1 结构体信息

  1. struct KeyNode
  2. {
  3. unsigned int major;
  4. struct class * cls;
  5. struct device * dev;
  6. unsigned int irqno;
  7. unsigned int * dat;
  8. int value;
  9. wait_queue_head_t head;
  10. unsigned int key_state;
  11. };
  12. struct KeyNode key;

7.5.2 应用程序接口和函数

  1. ssize_t key_read (struct file * filep, char __user * buf, size_t size, loff_t *fops)
  2. {
  3. printk("-------%s--------\n",__FUNCTION__);
  4. if(filep->f_flags & O_NONBLOCK && !key.key_state)//非阻塞
  5. {
  6. return -EAGAIN;
  7. }
  8. wait_event_interruptible(key.head,key.key_state);//阻塞
  9. copy_to_user(buf,&(key.value),size);
  10. key.key_state = 0;
  11. return 4;
  12. }
  13. int key_open (struct inode * inode, struct file * filep)
  14. {
  15. printk("-------%s--------\n",__FUNCTION__);
  16. return 0;
  17. }
  18. int key_release (struct inode * inode, struct file *filep)
  19. {
  20. printk("-------%s--------\n",__FUNCTION__);
  21. return 0;
  22. }
  23. const struct file_operations fops = {
  24. .release = key_release,
  25. .open = key_open,
  26. .read = key_read,
  27. };

7.5.3 入口函数

  1. static int __init key_drv_init(void)
  2. {
  3. key.major = register_chrdev(0,"key drv",&fops);
  4. key.cls = class_create(THIS_MODULE,"key cls");
  5. key.dev = device_create(key.cls,NULL,MKDEV(key.major,0),NULL,"key3");
  6. struct device_node * np = of_find_node_by_path("/key_int_node");
  7. key.irqno = irq_of_parse_and_map(np,0);
  8. request_irq(key.irqno,irq_handler,IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,"key irq",NULL);
  9. //创建等待队列
  10. init_waitqueue_head(&(key.head));
  11. key.dat = ioremap(0x11000c24,4);
  12. return 0;
  13. }

7.5.4 中断处理函数

  1. //中断处理函数----irqno
  2. irqreturn_t irq_handler(int irqno, void * dev)
  3. {
  4. printk("hello world\n");
  5. key.value = readl(key.dat) >> 2 & 1;
  6. key.key_state = 1;
  7. //唤醒
  8. wake_up_interruptible(&(key.head));
  9. return IRQ_HANDLED;
  10. }

7.5.5 应用程序

  1. #include <sys/types.h>
  2. #include <sys/stat.h>
  3. #include <fcntl.h>
  4. #include <unistd.h>
  5. #include <stdio.h>
  6. int main()
  7. {
  8. int i = -2;
  9. int fd = open("/dev/key3",O_RDONLY|O_NONBLOCK);
  10. /*
  11. int status = fcntl(fd,F_GETFL);
  12. status = status | O_NONBLOCK;
  13. fcntl(fd,F_SETFL,status);
  14. */
  15. while(1)
  16. {
  17. read(fd,&i,4);
  18. printf("%d\n",i);
  19. }
  20. close(fd);
  21. return 0;
  22. }

 8 文件IO模型(阻塞、非阻塞、IO多路复用、异步通知)

8.1 阻塞

阻塞等同于休眠,当进程在读取外部设备的资源(数据),如果资源没有准备,就会进行休眠等待

8.1.1 等待队列头

根据等待队列头创建等待队列

init_waitqueue_head(wait_queue_head_t * q);

 wait_queue_head_t是一个等待队列结构体,包含了一个等待队列的相关信息,如:

-    等待队列中的任务链表
-    等待队列的锁(用于多线程同步访问)
-    等待队列的计数器(记录当前等待队列内的任务数)

init_waitqueue_head函数初始化一个wait_queue_head_t类型的等待队列,具体工作如下:

1. 将等待队列头的任务链表初始化为空。
2. 初始化等待队列头的锁。
3. 将等待队列头的计数器重置为0。
4. 如果该等待队列头是一般等待队列(WQ_FLAG_EXCLUSIVE未置位),还需要初始化其万能链表。

简而言之,init_waitqueue_head函数主要对一个等待队列头的成员进行初始化,以便其能够正常运作。在驱动开发中,我们通常在模块加载时调用此函数对需要的等待队列头进行初始化。

 8.1.2 休眠等待

  1. wait_event_interruptible(wait_queue_head_t wq,condition)
  2. 用于将当前任务添加到一个等待队列中并睡眠,直到 condition 为真或被信号唤醒
  3. 参数1
  4. wait_queue_head_t wq:等待队列头
  5. 参数2
  6. condition:条件,为假,就会等待;为真,就不会等待

 wait_event_interruptible的主要工作流程如下:

1. 检查condition,如果为真则直接返回0。

2. 否则,将当前任务添加到等待队列wq的等待链表中。

3. 使当前任务睡眠,调度器选择其他任务运行。

4. 如果当前任务被信号唤醒,则从等待队列中移除任务,返回-ERESTARTSYS。

5. 当condition为真时,等待队列中的任务将被唤醒。等待队列将从链表中移除当前任务,并返回0。

6. 步骤1~5会重复,直到condition为真或被信号唤醒退出循环。

 简而言之,wait_event_interruptible函数用于实现条件睡眠,当前任务会一直睡眠直到condition满足或收到信号。它与等待队列结合,可以用于进程间同步与通信。

  1. //示例代码
  2. wait_queue_head_t wait_queue;
  3. int condition = 0;
  4. // 初始化等待队列头
  5. init_waitqueue_head(&wait_queue);
  6. // 休眠任务,等待condition为真
  7. wait_event_interruptible(wait_queue, condition);
  8. // 条件满足,唤醒等待队列中的任务
  9. condition = 1;
  10. wake_up(&wait_queue);

8.1.3 进程唤醒 

  1. wake_up_interruptible(wait_queue_head_t * q);
  2. *q:要唤醒的队列头

 wake_up_interruptible的主要工作流程如下:

1. 启用等待队列头q的锁,以保证多线程安全访问。

2. 遍历等待队列头q中的等待任务链表。

3. 对于每个任务,执行唤醒操作try_to_wake_up。

4. try_to_wake_up函数检查任务状态,如果是可中断睡眠(TASK_INTERRUPTIBLE)则将其唤醒。

5. 唤醒的任务将从等待队列头q的等待链表中移除。

6. 禁用等待队列头q的锁,退出流程。

 简而言之,wake_up_interruptible函数会尝试唤醒等待队列q中的所有处于可中断睡眠状态(TASK_INTERRUPTIBLE)的任务。被唤醒的任务将从等待队列的等待链表中移除,恢复运行。

8.2 非阻塞

在读写的时候,如果没有数据,就立即返回,应用需要设置为非阻塞

  1. int fd = open("/dev/key3",O_RDONLY|O_NONBLOCK);
  2. int status = fcntl(fd,F_GETFL);status = status | O_NONBLOCK;
  3. fcntl(fd,F_SETFL,status);

 驱动中需要区分当前模式是否为非阻塞模式:如果是非阻塞模式,且没有数据就立即返回

  1. if(filep->f_flags & O_NONBLOCK && !key.key_state)//非阻塞
  2. {
  3. return -EAGAIN;
  4. }
  1. fcntl函数用于操控文件描述符,它的原型为:
  2. int fcntl(int fd, int cmd, ...);
  3. 其主要参数为:
  4. - fd:文件描述符。
  5. - cmd:操作命令,指定对文件描述符fd进行的控制操作。
  6. - ...:cmd所需要的其他参数,取决于所指定的操作命令。
  7. 当cmd为F_GETFL时,fcntl函数的作用是获取文件描述符fd的文件状态标志。它不需要第三个参数,函数调用为:
  8. int status = fcntl(fd, F_GETFL);
  9. 该调用将返回fd文件的状态标志,通常存储在status变量中。状态标志是用于指定文件的属性和操作方式的一组宏定义,包括:
  10. - O_RDONLY: 只读文件
  11. - O_WRONLY: 只写文件
  12. - O_RDWR: 读写文件
  13. - O_APPEND: 追加模式
  14. - O_NONBLOCK: 非阻塞I/O
  15. - O_SYNC: 同步I/O
  16. 等等。
  17. 这些状态标志可以在打开文件时通过flags参数设置,也可以通过fcntl函数的F_SETFL操作在打开后动态修改。
  18. 例如,获取标准输入stdin的状态标志:
  19. int status = fcntl(STDIN_FILENO, F_GETFL);
  20. 设置文件描述符fd为非阻塞I/O:
  21. int flags = fcntl(fd, F_GETFL); // 获取原状态标志
  22. flags |= O_NONBLOCK; // 设置非阻塞标志
  23. fcntl(fd, F_SETFL, flags); // 设置状态标志

 8.3 异步通知

当有数据的时候,驱动就会发送信号给(SIGIO)给应用,应用就可以异步去读写数据,不用主动读写。


8.3.1 驱动编写

驱动用于发送信号,和进程进行关联,也就是信号发送给谁,通过fasync接口实现

  1. int key_fasync (int fd, struct file * filep, int on)
  2. {
  3. return fasync_helper(fd,filep,on,&(key.fasync));
  4. }
  1. int key_fasync(int fd, struct file *filep, int on);
  2. 参数:
  3. fd:文件描述符。
  4. filep:与文件描述符fd对应文件的file结构体指针。
  5. on:开启或取消asynchronous notification。
  6. 功能:
  7. 使用key_fasync设置异步通知后,应用程序无需轮询文件描述符,内核会在I/O完成后主动发送信号通知应用程序,从而提高程序的效率与响应速度。
  8. 返回值:
  9. 0:操作成功完成。
  10. 负值:操作失败,此时不会有任何异步通知的设置或取消动作。
  11. 可能的错误码有:
  12. - -EBADF:文件描述符fd无效。
  13. - -EINVAL:filp结构指针filep为空或filp与fd不 match。
  14. - -ENOMEM:内存分配失败。fasync_list链表使用kmalloc动态分配,如果分配失败将返回此错误。
  15. 详细作用描述:
  16. on为真时,key_fasync函数的作用是:
  17. 1. 根据文件描述符fd查找其对应的filp(file结构体指针),通常由filep参数提供。
  18. 2. 将该filp添加到fasync_list链表,该链表包含所有请求异步通知的filp。
  19. 3. 当文件可读、写或出错时,内核会遍历fasync_list链表,调用程序注册的信号驱动函数,发送相应的信号给进程。
  20. 4. 从而实现I/O完成后立即通知应用程序的效果。
  21. on为假时,key_fasync函数的作用是:
  22. 1. 从fasync_list链表中移除与文件描述符fd对应的filp。
  23. 2. 取消该文件描述符的异步通知请求。
  1. //然后在某个特定的时刻(比如有数据的时候)发送信号
  2. kill_fasync(&(key.fasync),SIGIO,POLLIN);
  1. void kill_fasync(struct fasync_struct **fp, int sig, int band);
  2. 其主要参数为:
  3. - fp:fasync_struct结构体指针的指针,指向I/O通知链表。
  4. - sig:要发送的信号,一般为SIGIO。
  5. - band:I/O操作的类型,如POLLIN、POLLOUT等。
  6. 功能:
  7. kill_fasync函数根据 band参数检查链表fp上的每个文件描述符是否请求了相应的I/O监视,如果是则发送sig信号通知应用进程相应的I/O事件已就绪。
  8. kill_fasync函数的主要工作流程如下:
  9. 1. 遍历I/O通知链表fp所指向的fasync_struct结构体链表。
  10. 2. 对于每个fasync_struct,检查其文件描述符的I/O监视事件是否包含band指定的I/O操作。
  11. - 如果不包含,继续遍历下一结构体。
  12. - 如果包含,构造siginfo结构体,发送sig信号给fasync_struct->fa_file->f_owner所指定的进程。
  13. 3. 发送信号通知应用程序,I/O操作band已就绪。
  14. 4. 退出,异步通知完成。

8.3.2 应用程序编写

  1. 1、设置信号怎么处理
  2. signal(SIGIO,catch_signal);
  3. void catch_signal(int signo)
  4. {
  5. if(signo == SIGIO)
  6. {
  7. int value = -1;
  8. read(fd,&value,4);
  9. printf("program data is %d\n",value);
  10. }
  11. }
  1. 2、将当前进程设置为SIGIO的属主进程
  2. //设置当前进程为信号的属主进程
  3. fcntl(fd,F_SETOWN,getpid());
  1. 3、将io模式设置为异步模式
  2. int stats = fcntl(fd,F_GETFL);
  3. stats = stats | FASYNC;
  4. fcntl(fd,F_SETFL,stats);

8.4 示例代码 

  1. //key_test_tasklet.c
  2. #include <sys/types.h>
  3. #include <sys/stat.h>
  4. #include <fcntl.h>
  5. #include <unistd.h>
  6. #include <stdio.h>
  7. #include <signal.h>
  8. int fd;
  9. void catch_signal(int signo)
  10. {
  11. if(signo == SIGIO)
  12. {
  13. int value = -1;
  14. read(fd,&value,4);
  15. printf("program data is %d\n",value);
  16. }
  17. }
  18. int main()
  19. {
  20. int i = -2;
  21. fd = open("/dev/key3",O_RDONLY);
  22. //信号处理方式
  23. signal(SIGIO,catch_signal);
  24. //设置当前进程为信号的属主进程
  25. fcntl(fd,F_SETOWN,getpid());
  26. //将io模式设置为异步模式
  27. int stats = fcntl(fd,F_GETFL);
  28. stats = stats | FASYNC;
  29. fcntl(fd,F_SETFL,stats);
  30. while(1)
  31. {
  32. printf("hello world\n");
  33. sleep(1);
  34. }
  35. close(fd);
  36. return 0;
  37. }

  1. //key_drv_tasklet.c
  2. #include <linux/init.h>
  3. #include <linux/module.h>
  4. #include <linux/of.h>
  5. #include <linux/of_irq.h>
  6. #include <linux/interrupt.h>
  7. #include <linux/slab.h>
  8. #include <linux/fs.h>
  9. #include <linux/device.h>
  10. #include <asm/io.h>
  11. #include <asm/poll.h>
  12. #include <asm/uaccess.h>
  13. #include <linux/wait.h>
  14. #include <linux/sched.h>
  15. struct KeyNode
  16. {
  17. unsigned int major;
  18. struct class * cls;
  19. struct device * dev;
  20. unsigned int irqno;
  21. unsigned int * dat;
  22. int value;
  23. wait_queue_head_t head;
  24. unsigned int key_state;
  25. struct fasync_struct * fasync;
  26. struct tasklet_struct tasklet;
  27. };
  28. struct KeyNode key;
  29. ssize_t key_read (struct file * filep, char __user * buf, size_t size, loff_t *fops)
  30. {
  31. printk("-------%s--------\n",__FUNCTION__);
  32. if(filep->f_flags & O_NONBLOCK && !key.key_state)//非阻塞
  33. {
  34. return -EAGAIN;
  35. }
  36. wait_event_interruptible(key.head,key.key_state);//阻塞
  37. copy_to_user(buf,&(key.value),size);
  38. key.key_state = 0;
  39. return 4;
  40. }
  41. int key_open (struct inode * inode, struct file * filep)
  42. {
  43. printk("-------%s--------\n",__FUNCTION__);
  44. return 0;
  45. }
  46. int key_release (struct inode * inode, struct file *filep)
  47. {
  48. printk("-------%s--------\n",__FUNCTION__);
  49. return 0;
  50. }
  51. int key_fasync (int fd, struct file * filep, int on)
  52. {
  53. return fasync_helper(fd,filep,on,&(key.fasync));
  54. }
  55. const struct file_operations fops = {
  56. .release = key_release,
  57. .open = key_open,
  58. .read = key_read,
  59. .fasync = key_fasync,
  60. };
  61. //中断下半部分执行操作
  62. void irq_func(unsigned long data)
  63. {
  64. printk("data is %d\n",data);
  65. }
  66. //中断处理函数----irqno
  67. irqreturn_t irq_handler(int irqno, void * dev)
  68. {
  69. printk("hello world\n");
  70. key.value = readl(key.dat) >> 2 & 1;
  71. key.key_state = 1;
  72. //唤醒
  73. wake_up_interruptible(&(key.head));
  74. //发送信号
  75. kill_fasync(&(key.fasync),SIGIO,POLLIN);
  76. //中断下半部分启动--放入内核线程
  77. tasklet_schedule(&(key.tasklet));
  78. return IRQ_HANDLED;
  79. }
  80. static int __init key_drv_init(void)
  81. {
  82. key.major = register_chrdev(0,"key drv",&fops);
  83. key.cls = class_create(THIS_MODULE,"key cls");
  84. key.dev = device_create(key.cls,NULL,MKDEV(key.major,0),NULL,"key3");
  85. struct device_node * np = of_find_node_by_path("/key_int_node");
  86. key.irqno = irq_of_parse_and_map(np,0);
  87. request_irq(key.irqno,irq_handler,IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,"key irq",NULL);
  88. //初始化中断下半部分任务(struct tasklet_struct 结构体初始化)
  89. tasklet_init(&(key.tasklet),irq_func,45);
  90. //创建等待队列
  91. init_waitqueue_head(&(key.head));
  92. key.dat = ioremap(0x11000c24,4);
  93. return 0;
  94. }
  95. static void __exit key_drv_exit(void)
  96. {
  97. }
  98. module_init(key_drv_init);
  99. module_exit(key_drv_exit);
  100. MODULE_LICENSE("GPL");

9 中断下半部分

当中断处理程序比较长时,为了提高相应特性,linux内核处理中断的方案是:人为地将处理程序分为两部分,即中断上半部、中断下半部。其中中断上半部标记中断,调度下半部;下半部负责真正的操作(比如读取按键键值、从网卡读取缓冲数据等)

 9.1 中断下半部的两种实现机制

9.1.1 tasklet机制(小任务机制)

内部实现调用sortirq

sorfirq:处理速度比较快,但是是内核级别的机制,要修改内核源码,不推荐使用

 tasklet在中断上下文执行,因此不能被阻塞,不能睡眠,不能被打断

编程:

1. 初始化,设置tasklet任务结构体
struct tasklet_struct tasklet;
tasklet_init(&任务结构体的地址,中断下半部分执行函数,函数参数);
2. 启动中断下半部分----把任务放入到内核线程中
//中断下半部分启动--放入内核线程
tasklet_schedule(&tasklet);

9.1.2 workqueue机制(工作队列机制)

workqueue在进程上下文,可以被重新调度,可以阻塞,也可以睡眠

编程:

1. 初始化,设置workqueue任务结构体
struct work_struct workqueue;
INIT_WORK(&任务结构体的地址,中断下半部分执行函数);
2. 启动中断下半部分----把任务放入到内核线程中
//中断下半部分启动--放入内核线程
schedule_work(&workqueue);

9.2 利用workqueue示例代码

  1. //key_drv_queue.c
  2. #include <linux/init.h>
  3. #include <linux/module.h>
  4. #include <linux/of.h>
  5. #include <linux/of_irq.h>
  6. #include <linux/interrupt.h>
  7. #include <linux/slab.h>
  8. #include <linux/fs.h>
  9. #include <linux/device.h>
  10. #include <asm/io.h>
  11. #include <asm/poll.h>
  12. #include <asm/uaccess.h>
  13. #include <linux/wait.h>
  14. #include <linux/sched.h>
  15. struct KeyNode
  16. {
  17. unsigned int major;
  18. struct class * cls;
  19. struct device * dev;
  20. unsigned int irqno;
  21. unsigned int * dat;
  22. int value;
  23. wait_queue_head_t head;
  24. unsigned int key_state;
  25. struct fasync_struct * fasync;
  26. struct work_struct work;
  27. };
  28. struct KeyNode key;
  29. ssize_t key_read (struct file * filep, char __user * buf, size_t size, loff_t *fops)
  30. {
  31. printk("-------%s--------\n",__FUNCTION__);
  32. if(filep->f_flags & O_NONBLOCK && !key.key_state)//非阻塞
  33. {
  34. return -EAGAIN;
  35. }
  36. wait_event_interruptible(key.head,key.key_state);//阻塞
  37. copy_to_user(buf,&(key.value),size);
  38. key.key_state = 0;
  39. return 4;
  40. }
  41. int key_open (struct inode * inode, struct file * filep)
  42. {
  43. printk("-------%s--------\n",__FUNCTION__);
  44. return 0;
  45. }
  46. int key_release (struct inode * inode, struct file *filep)
  47. {
  48. printk("-------%s--------\n",__FUNCTION__);
  49. return 0;
  50. }
  51. int key_fasync (int fd, struct file * filep, int on)
  52. {
  53. return fasync_helper(fd,filep,on,&(key.fasync));
  54. }
  55. const struct file_operations fops = {
  56. .release = key_release,
  57. .open = key_open,
  58. .read = key_read,
  59. .fasync = key_fasync,
  60. };
  61. //中断下半部分执行操作
  62. void work_func(struct work_struct *work)
  63. {
  64. printk("nihao\n");
  65. }
  66. //中断处理函数----irqno
  67. irqreturn_t irq_handler(int irqno, void * dev)
  68. {
  69. printk("hello world\n");
  70. key.value = readl(key.dat) >> 2 & 1;
  71. key.key_state = 1;
  72. //唤醒
  73. wake_up_interruptible(&(key.head));
  74. //发送信号
  75. kill_fasync(&(key.fasync),SIGIO,POLLIN);
  76. //中断下半部分启动--放入内核线程(不是在中断中执行任务,而是把任务加入到内核,在内核执行)
  77. schedule_work(&(key.work));
  78. return IRQ_HANDLED;
  79. }
  80. static int __init key_drv_init(void)
  81. {
  82. key.major = register_chrdev(0,"key drv",&fops);
  83. key.cls = class_create(THIS_MODULE,"key cls");
  84. key.dev = device_create(key.cls,NULL,MKDEV(key.major,0),NULL,"key3");
  85. struct device_node * np = of_find_node_by_path("/key_int_node");
  86. key.irqno = irq_of_parse_and_map(np,0);
  87. request_irq(key.irqno,irq_handler,IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,"key irq",NULL);
  88. //初始化中断下半部分任务(struct work_struct 结构体初始化)
  89. INIT_WORK(&(key.work),work_func);
  90. //创建等待队列
  91. init_waitqueue_head(&(key.head));
  92. key.dat = ioremap(0x11000c24,4);
  93. return 0;
  94. }
  95. static void __exit key_drv_exit(void)
  96. {
  97. }
  98. module_init(key_drv_init);
  99. module_exit(key_drv_exit);
  100. MODULE_LICENSE("GPL");
  1. //key_test_queue.c
  2. #include <sys/types.h>
  3. #include <sys/stat.h>
  4. #include <fcntl.h>
  5. #include <unistd.h>
  6. #include <stdio.h>
  7. #include <signal.h>
  8. int fd;
  9. void catch_signal(int signo)
  10. {
  11. if(signo == SIGIO)
  12. {
  13. int value = -1;
  14. read(fd,&value,4);
  15. printf("program data is %d\n",value);
  16. }
  17. }
  18. int main()
  19. {
  20. int i = -2;
  21. fd = open("/dev/key3",O_RDONLY);
  22. //信号处理方式
  23. signal(SIGIO,catch_signal);
  24. //设置当前进程为信号的属主进程
  25. fcntl(fd,F_SETOWN,getpid());
  26. //将io模式设置为异步模式
  27. int stats = fcntl(fd,F_GETFL);
  28. stats = stats | FASYNC;
  29. fcntl(fd,F_SETFL,stats);
  30. while(1)
  31. {
  32. printf("hello world\n");
  33. sleep(1);
  34. }
  35. close(fd);
  36. return 0;
  37. }

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/180471
推荐阅读
相关标签
  

闽ICP备14008679号