linux内核工作队列
来源:互联网 发布:mac解压软件哪个好 编辑:程序博客网 时间:2024/04/30 10:20
内核工作队列概述
工作队列(workqueue
)是另外一种将工作推后执行的形式,工作队列可以把工作推后,交由一个内核线程去执行,也就是说,这个下半部分可以在进程上下文中执行,最重要的就是工作队列允许被重新调度甚至睡眠。
linux workqueue工作原理
linux系统启动期间会创建名为kworker/u:x
(x
是0
开始的整数,表示CPU
编号)工作者内核线程,该线程创建之后处于sleep
状态。从调度器的角度来解释,内核线程就是可以调度的进程;从代码表现形式看,本质是一个函数。
工作队列结构原理
work_struct
,workqueue_struct
,struct cpu_workqueue_struct
三者之间关系如下:
- 内核启动时会为每一个
CPU
创建一个cpu_workqueue_struct
结构,同时还会有一个内核工作的线程,这个线程创建好后处于睡眠状态,等待用户加入工作,来唤醒线程去调度工作结构体work_struct
中的工作函数。 - 工作
work_struct
是通过链表连接在cpu_workqueue_struct
上,后面其他work_struct
连接在前一个后面,组成一个队列 - 工作者线程被唤醒后,会去自己负责的工作队列上依次执行上面
struct_work
结构中的工作函数,执行完成后就会把work_struct
从链表上删除。 - 如果想使用工作队列来延后执行一段代码,必须先创建
work_struct
->
cpu_workqueue_struct
,然后把工作节点work_struct
加入到workqueue_struct
工作队列中,加入后工作者线程就会被唤醒,在适当的时机就会执行工作函数。
工作队列数据结构
不同版本内核中,对数据结构定义是不同的,以下是linux3.5
源码版本摘出来的数据结构
work_struct
我们把推后执行的任务叫工作(work
),描述它的数据结构为work_struct
路径:workqueue.h linux-3.5\include\linux
struct work_struct { atomic_long_t data; struct list_head entry; //链表指针 把每个工作连接在一个链表上组成一个双向链表 work_func_t func; //函数指针 指向工作函数#ifdef CONFIG_LOCKDEP struct lockdep_map lockdep_map;#endif};
补充work_func_t
结构
typedef void (*work_func_t)(void *work);
补充list_head
结构
struct list_head { struct list_head *next, *prev;};
我们编程只需要关注func成员它是工作函数指针就是用户需要延后执行的代码
workqueue_struct
这个结构是用来描述内核队列的数据结构。定义在workqueue.c linux-3.5\kernel
中,在workqueue.h linux-3.5\include\linux
中声明
具体定义如下:
struct workqueue_struct { unsigned int flags; /* W: WQ_* flags */ union { struct cpu_workqueue_struct __percpu *pcpu; struct cpu_workqueue_struct *single; unsigned long v; } cpu_wq; /* I: cwq's */ struct list_head list; /* W: list of all workqueues */ struct mutex flush_mutex; /* protects wq flushing */ int work_color; /* F: current work color */ int flush_color; /* F: current flush color */ atomic_t nr_cwqs_to_flush; /* flush in progress */ struct wq_flusher *first_flusher; /* F: first flusher */ struct list_head flusher_queue; /* F: flush waiters */ struct list_head flusher_overflow; /* F: flush overflow list */ mayday_mask_t mayday_mask; /* cpus requesting rescue */ struct worker *rescuer; /* I: rescue worker */ int nr_drainers; /* W: drain in progress */ int saved_max_active; /* W: saved cwq max_active */#ifdef CONFIG_LOCKDEP struct lockdep_map lockdep_map;#endif char name[]; /* I: workqueue name */};
注意:这个结构表示一个工作队列,一般情况下驱动开发者不需要接触太多这个结构成员,关于队列操作,内核都提供了相应的API
函数
cpu_workqueue_struct
struct cpu_workqueue_struct { struct global_cwq *gcwq; /* I: the associated gcwq */ struct workqueue_struct *wq; /* I: the owning workqueue */ int work_color; /* L: current color */ int flush_color; /* L: flushing color */ int nr_in_flight[WORK_NR_COLORS]; /* L: nr of in_flight works */ int nr_active; /* L: nr of active works */ int max_active; /* L: max active works */ struct list_head delayed_works; /* L: delayed works */};
内核通过delayed_works
成员把第一个 work_struct
连接起来,后面work_struct
通过本身的entry
成员把自己连接在链表上。
内核工作队列分类
内核工作队列分成共享工作队列和自定义工作队列两种
共享工作队列
系统在启动时候自动创建一个工作队列驱动开发者如果想使用这个队列,则不需要自己创建工作队列,只需要把自己的work
添加到这个工作队列上即可。
使用schedule_work
这个函数可以把work_struct
添加到工作队列中
自定义工作队列
由于共享工作队列是大家共同使用的,如果上面的工作函数有存在睡眠的情况,阻塞了,则会影响到后面挂接上去的工作执行时间,当你的动作需要尽快执行,不想受其它工作函数的影响,则自己创建一个工作队列,然后把自己的工作添加到这个自定义工作队列上去。
使用自定义工作队列分为两步:
- 创建工作队列:使用
creat_workqueue(name)
创建一个名为name
的工作队列 - 把工作添加到上面创建的工作队列上:使用
queue_work
函数把一个工作结构work_struc
添加到指定的工作队列上
linux内核共享工作队列
共享工作队列介绍
内核为了方便驱动开发者使用工作队列,给我们创建好一个工作队列,只要使用schedule_work
添加的工作节点都是添加到内核共享工作队列中,使用方法只需要开发者实现一个 work_struct
结构,然后把它添加到共享工作中去。
内核共享队列API
静态定义工作结构DECLARE_WORK
路径workqueue.h \linux-3.5\include\linux
#define DECLARE_WORK(n, f) \ struct work_struct n = __WORK_INITIALIZER(n, f)
功能:定义一个名字为n
的work_struct
结构变量,并且初始化它,工作是f
。
参数:n
要定义的work_struct
结构变量名,f
工作函数,要延后执行的代码
动态初始化工作结构INIT_WORK
路径workqueue.h \linux-3.5\include\linux
#define INIT_WORK(_work, _func) \ do { \ __INIT_WORK((_work), (_func), 0); \ } while (0)
功能:运行期间动态初始化work_struct
结构
参数:_work
要定义的work_struct
结构变量地址,_func
工作函数,要延后执行的代码
调度工作schedule_work
声明路径在workqueue.h \linux-3.5\include\linux
int queue_work(struct workqueue_struct *wq, struct work_struct *work){ int ret; ret = queue_work_on(get_cpu(), wq, work); put_cpu(); return ret;}
功能:把一个work_struct
添加道共享工作队列中,成为一个工作节点。
参数:work
要定义的work_struct
结构变量名地址
返回值:0
表示已经挂接到共享工作队列上还未执行。非0
其他情况内核未做说明
函数返回值通常不需要驱动开发者关注
共享队列的使用步骤
- 需要工作队列
- 创建工作
- 调度工作(创建工作节点)
对于共享工作队列来讲,第一部已经有了,需要做第2/3
步
编程步骤
- 编写一个工作函数
原型void (*work_func_t)(void *work);
void mywork_func(struct work_struct *work){······//工作内容}
工作函数的参数是工作结构变量的首地址
- 定义一个工作结构
第一种:静态定义
DECLARE_WORK(mywork,mywork_func); //定义并且初始化
第二种:动态定义
struct work_struct mywork;
- 初始化上一步定义的工作结构
对于静态定义可以略过这一步,使用动态方式定义需要手动初始化INIT_WORK(&mywork,mywork_func);
- 在适当的地方调度工作,把工作结构添加到工作队列上
schedule_work(&mywork);
内核共享队列示例
#include<linux/module.h>#include<linux/init.h>//添加头文件#include<linux/workqueue.h>//实现一个work_func工作函数void mywork_func(struct work_struct *work){ printk("%s is call!! work:%p\r\n",__FUNCTION__,work);}//定义一个struct work_struct结构变量,并且进行初始化DECLARE_WORK(mywork,mywork_func); //定义并且初始化static int __init mywork_init(void){ //一安装模块就进行调度 schedule_work(&mywork); printk("%s is call!!",__FUNCTION__); return 0;}static void __exit mywork_exit(void){ printk("mywork is exit!\r\n");}module_init(mywork_init);module_exit(mywork_exit);MODULE_LICENSE("GPL");
Makefile
KERN_DIR = /zhangchao/linux3.5/linux-3.5all: make -C $(KERN_DIR) M=`pwd` modulesclean: make -C $(KERN_DIR) M=`pwd` modules clean rm -rf modules.orderobj-m += workqueue.o
开发板运行效果
[root@ZC/zhangchao]#insmod workqueue.ko [ 3526.610000] mywork_init is call!![ 3526.615000] mywork_func is call!! work:bf0040c0
验证工作函数的参数是工作结构变量的首地址
验证代码
#include<linux/module.h>#include<linux/init.h>//添加头文件#include<linux/workqueue.h>//实现一个work_func工作函数void mywork_func(struct work_struct *work){ printk("%s is call!! work:%p\r\n",__FUNCTION__,work);}//定义一个struct work_struct结构变量,并且进行初始化DECLARE_WORK(mywork,mywork_func); //定义并且初始化static int __init mywork_init(void){ //一安装模块就进行调度 schedule_work(&mywork); printk("&mywork:%p\r\n",&mywork); printk("%s is call!!\r\n",__FUNCTION__); return 0;}static void __exit mywork_exit(void){ printk("mywork is exit!\r\n");}module_init(mywork_init);module_exit(mywork_exit);MODULE_LICENSE("GPL");
开发板演示效果
[root@ZC/zhangchao]#insmod workqueue.ko [ 4208.410000] &mywork:bf0080c4[ 4208.410000] mywork_init is call!!mywork_func is call!! work:bf0080c4
运行结果表明:工作函数的参数是工作结构变量的首地址
为什么将工作结构体指针传入工作函数内部呢?用一个全局变量直接赋值不更方便么?原来在实际驱动开发中,经常将工作结构封装进我们自定义的数据结构中,这个数据结构还有其他成员,将数据结构地址传入,相比全局变量应用更加方便,同时减少了全局变量的使用,在一定意义上方便了驱动开发人员编写代码。
示例代码:
#include<linux/module.h>#include<linux/init.h>//添加头文件#include<linux/workqueue.h>//定义一个自定义数据结构 , 将工作结构体包含其中struct my_data {struct work_struct mywork;int x;int y;int z;};struct my_data mydata;//实现一个work_func工作函数void mywork_func(struct work_struct *work){ struct my_data *p=(struct my_data *)work; printk("p->mywork is %p\r\n",&p->mywork); printk("x:%d y:%d z:%d\r\n",mydata.x,mydata.y,mydata.z); printk("p->x:%d,p->y:%d,p->z:%d\r\n",p->x,p->y,p->z);}//定义一个struct work_struct结构变量,并且进行初始化//DECLARE_WORK(mywork,mywork_func); //定义并且初始化static int __init mywork_init(void){ //初始化相关变量 mydata.x=123; mydata.y=456; mydata.z=789; //工作结构初始化 INIT_WORK(&mydata.mywork,mywork_func); //一安装模块就进行调度 schedule_work(&mydata.mywork); printk("mywork:%p\r\n",&mydata.mywork); printk("%s is call!!\r\n",__FUNCTION__); return 0;}static void __exit mywork_exit(void){ printk("mywork is exit!\r\n");}module_init(mywork_init);module_exit(mywork_exit);MODULE_LICENSE("GPL");
开发板演示效果:
[root@ZC/zhangchao]#insmod workqueue.ko [ 615.650000] p->mywork is bf004280[ 615.650000] x:123 y:456 z:789[ 615.650000] p->x:123,p->y:456,p->z:789[ 615.650000] mywork:bf004280[ 615.650000] mywork_init is call!!
我们能观察到,通过全局变量打印的结果与通过指针传入打印的结果是相同的,如果采用指针传入的方式可以避免全局变量的使用,方便实用。由于传入的work工作结构恰好是结构体第一个成员,传入工作结构地址也就是传入整个结构的首地址,如果工作结构不是第一个成员那要怎样处理呢?
驱动代码:
#include<linux/module.h>#include<linux/init.h>//添加头文件#include<linux/workqueue.h>//定义一个自定义数据结构 , 将工作结构体包含其中 在第二位struct my_data {int x;struct work_struct mywork;int y;int z;};//实现一个work_func工作函数void mywork_func(struct work_struct *work){ struct my_data *p=(struct my_data *)((unsigned int)work-4); //打印工作数据结构地址 printk("p->mywork is %p\r\n",&p->mywork); printk("p->x:%d,p->y:%d,p->z:%d\r\n",p->x,p->y,p->z);}//定义一个struct work_struct结构变量,并且进行初始化//DECLARE_WORK(mywork,mywork_func); //定义并且初始化static int __init mywork_init(void){ //不使用全局变量 在函数其中定义 struct my_data mydata; //初始化相关变量 mydata.x=123; mydata.y=456; mydata.z=789; //工作结构初始化 INIT_WORK(&mydata.mywork,mywork_func); //一安装模块就进行调度 schedule_work(&mydata.mywork); //打印结构体首地址 printk("mydata.x:%p\r\n",&mydata.x); printk("%s is call!!\r\n",__FUNCTION__); return 0;}static void __exit mywork_exit(void){ printk("mywork is exit!\r\n");}module_init(mywork_init);module_exit(mywork_exit);MODULE_LICENSE("GPL");
开发板运行效果:
[root@ZC/zhangchao]#insmod workqueue.ko [ 1437.705000] p->mywork is ed063ea0[ 1437.705000] p->x:123,p->y:456,p->z:789[ 1437.705000] mydata.x:ed063e9c[ 1437.705000] mywork_init is call!!
运行结果表明虽然传入的工作结构并不是数据结构的第一个成员,但是在工作函数中还原地址时,只需要将前面成员所占用地址减去就能得到数据结构的首地址,打印的结果也不会出现错误。但是!!这是在数据成员不复杂的情况下可以计算得出数据结构的首地址,如果数据结构成员很复杂、很多,计算起来就会很麻烦而且容易出错,因此,计算的办法不太好,怎么做?内核提供了一个计算宏可以帮我们算出成员所在数据结构的首地址
首地址计算宏
#define container_of(ptr, type, member) ({ \ const typeof(((type *)0)->member) * __mptr = (ptr); \ (type *)((char *)__mptr - offsetof(type, member)); })#endif
功能:算出成员所在数据结构的首地址
参数:
ptr:已知结构的变量成员地址(&mydata.mywork
)
type:已知结构所在结构的变量类型(struct my_data
)
member:结构体成员名(mywork
)
驱动代码
#include<linux/module.h>#include<linux/init.h>//添加头文件#include<linux/workqueue.h>//定义一个自定义数据结构 , 将工作结构体包含其中 在第二位struct my_data {int x;struct work_struct mywork;int y;int z;};struct my_data mydata;//实现一个work_func工作函数void mywork_func(struct work_struct *work){ struct my_data *p=(struct my_data *)container_of(work,struct my_data,mywork); //打印工作数据结构地址 printk("p->mywork is %p\r\n",&p->mywork); printk("p->x:%d,p->y:%d,p->z:%d\r\n",p->x,p->y,p->z);}//定义一个struct work_struct结构变量,并且进行初始化//DECLARE_WORK(mywork,mywork_func); //定义并且初始化static int __init mywork_init(void){ //初始化相关变量 mydata.x=123; mydata.y=456; mydata.z=789; //工作结构初始化 INIT_WORK(&mydata.mywork,mywork_func); //一安装模块就进行调度 schedule_work(&mydata.mywork); //打印结构体首地址 printk("mydata.x:%p\r\n",&mydata.x); printk("%s is call!!\r\n",__FUNCTION__); return 0;}static void __exit mywork_exit(void){ printk("mywork is exit!\r\n");}module_init(mywork_init);module_exit(mywork_exit);MODULE_LICENSE("GPL");
开发板运行效果:
[root@ZC/zhangchao]#insmod workqueue.ko [ 3151.620000] mydata.x:bf010258[ 3151.620000] mywork_init is call!![ 3151.625000] p->mywork is bf01025c[ 3151.625000] p->x:123,p->y:456,p->z:789[root@ZC/zhangchao]#
结果表明这个宏能起到计算首地址效果
自定义内核工作队列
自定义工作队列介绍
根据前面的框架图可以知道,需要我们自己先创建一个工作队列,再往工作队列中添加工作结构。
自定义工作队列API
- create_workqueue(name)
会在每个CPU
创建一个cpu_workqueue_struct
结构
路径:workqueue.h linux-3.5\include\linux
#define create_workqueue(name) \ alloc_workqueue((name), WQ_MEM_RECLAIM, 1)
补充alloc_workqueue:
#ifdef CONFIG_LOCKDEP#define alloc_workqueue(fmt, flags, max_active, args...) \({ \ static struct lock_class_key __key; \ const char *__lock_name; \ \ if (__builtin_constant_p(fmt)) \ __lock_name = (fmt); \ else \ __lock_name = #fmt; \ \ __alloc_workqueue_key((fmt), (flags), (max_active), \ &__key, __lock_name, ##args); \})#else#define alloc_workqueue(fmt, flags, max_active, args...) \ __alloc_workqueue_key((fmt), (flags), (max_active), \ NULL, NULL, ##args)#endif
作用:创建一个名为name的工作队列
参数:工作队列的名字是一个字符串
返回值:
成功:创建的工作队列指针struct workqueue_struct *
失败:返回NULL
create_singlethread_workqueue(name)
作用和上面的相同区别在于,如果只创建一个cpu_workqueue_struct
结构,其余属性也相同queue_work
int queue_work(struct workqueue_struct *wq, struct work_struct *work);
作用:往wq
工作队列中添加work
工作节点
参数:wq
自己定义的工作队列结构变量地址,work
自己要添加的工作队列节点
返回值:
其实系统工作队列调用的也是这个函数,只不过系统工作队列只有一个在函数内部封装起来,不用我们自己去填写
int schedule_work(struct work_struct *work){ return queue_work(system_wq, work);}
- destory_workqueue
当用户不需要自定义工作队列时,必须使用这个函数来销毁这个工作队列,释放资源。void destroy_workqueue(struct workqueue_struct *wq)
作用:销毁自定义工作队列
参数:wq
自定一工作队列的指针
驱动代码
#include<linux/module.h>#include<linux/init.h>//添加头文件#include<linux/workqueue.h>//定义一个自定义数据结构 , 将工作结构体包含其中 在第二位struct my_data {int x;struct work_struct mywork;int y;int z;};struct my_data mydata;//创建一个自定义工作队列指针 用来承接创建等待队列成功的地址struct workqueue_struct *mywq;//实现一个work_func工作函数void mywork_func(struct work_struct *work){ struct my_data *p=(struct my_data *)container_of(work,struct my_data,mywork); //打印工作数据结构地址 printk("p->mywork is %p\r\n",&p->mywork); printk("p->x:%d,p->y:%d,p->z:%d\r\n",p->x,p->y,p->z);}//定义一个struct work_struct结构变量,并且进行初始化//DECLARE_WORK(mywork,mywork_func); //定义并且初始化static int __init mywork_init(void){ //初始化相关变量 mydata.x=123; mydata.y=456; mydata.z=789; //创建自定义工作队列 mywq=create_workqueue("mywq"); if( mywq ==NULL) { printk("create_workqueue error\r\n"); return -1; } printk("create_workqueue ok\r\n"); //工作结构初始化 INIT_WORK(&mydata.mywork,mywork_func); //一安装模块就进行调度 //schedule_work(&mydata.mywork); queue_work(mywq,&mydata.mywork); //打印结构体首地址 printk("mydata.x:%p\r\n",&mydata.x); printk("%s is call!!\r\n",__FUNCTION__); return 0;}static void __exit mywork_exit(void){ printk("mywork is exit!\r\n"); destroy_workqueue(mywq);}module_init(mywork_init);module_exit(mywork_exit);MODULE_LICENSE("GPL");
开发板运行效果
[root@ZC/zhangchao]#insmod workqueue.ko [ 287.095000] create_workqueue ok[ 287.095000] mydata.x:bf0002ac[ 287.095000] mywork_init is call!![ 287.095000] p->mywork is bf0002b0[ 287.095000] p->x:123,p->y:456,p->z:789
延时工作队列
与前面说的共享队列,还是自定义工作队列不同,延时队列是在调度时,需要等待指定时间才会调用工作函数。
延时队列数据结构
路径:workqueue.h linux-3.5\include\linux
内核使用一个delayed_work
结构来描述一个要延期执行的工作,其定义如下:
struct delayed_work { struct work_struct work; struct timer_list timer;};
从结构可以知道:延时工作队列就是把普通工作队列结构和一个内核定时器结合在一起实现延长指定时间的才调度。
延时队列相关API
- DECLARE_DELAYED_WORK
路径:workqueue.h linux-3.5\include\linux
#define DECLARE_DELAYED_WORK(n, f) \ struct delayed_work n = __DELAYED_WORK_INITIALIZER(n, f)
功能:定义一个名字为n
的delayed_work
结构变量,并且初始化它,工作函数是f
参数:
n:要定义的delayed_work
结构变量名
f:工作函数,就是要延期执行的代码
- INIT_DELAYED_WORK
#define INIT_DELAYED_WORK(_work, _func) \ do { \ INIT_WORK(&(_work)->work, (_func)); \ init_timer(&(_work)->timer); \ } while (0)
功能:运行期间动态初始化一个delayed_struct
结构
参数:
_work:初始化的要定义的delayed_struct
结构
_func:工作函数,要延期执行的代码
- int schedule_delayed_work
int schedule_delayed_work(struct delayed_work *work, unsigned long delay);
功能:把一个delayed_struct
添加到延时共享工作队列中,成为一个工作节点。
参数:
work:定义的delayed_struct
结构地址
delay:延时时间,时间单位时钟节拍jiffies
返回值:0
表示已经挂接到共享工作队列上还未执行。非0
其他情况内核未做说明 - queue_delayed_work
int queue_delayed_work(struct workqueue_struct *wq, struct delayed_work *work, unsigned long delay);
功能:把一个delayed_struct
添加到自定义延时工作队列中,成为一个工作节点。
参数:
wq:自定义的延时工作队列结构地址
work:定义的delayed_struct
结构地址
delay:延时时间,时间单位时钟节拍jiffies
返回值:0
表示已经挂接到共享工作队列上还未执行。非0
延时工作队列示例(使用内核共享工作队列)
软件编程思想
- 定义工作函数
- 定义一个延时工作结构
- 初始化上面定义的延时工作结构
- 在适当的地方调度已经初始化好的延时工作结构
编写代码 最简单的演示等待队列代码
#include<linux/module.h>#include<linux/init.h>//添加头文件#include<linux/workqueue.h>//实现一个work_func工作函数void mywork_func(struct work_struct *work){ printk("12345678\r\n");}//分配结构体//struct delayed_work mydelayed_work; 动态初始化DECLARE_DELAYED_WORK(mydelayed_work, mywork_func);static int __init mywork_init(void){ //一安装模块就进行调度 schedule_delayed_work(&mydelayed_work,2*HZ); printk("%s is call!!\r\n",__FUNCTION__); return 0;}static void __exit mywork_exit(void){ printk("mywork is exit!\r\n");}struct delayed_work;module_init(mywork_init);module_exit(mywork_exit);MODULE_LICENSE("GPL");
开发板演示效果
[root@ZC/zhangchao]#insmod workqueue.ko [ 5573.260000] mywork_init is call!![root@ZC/zhangchao]#[ 5575.265000] 12345678
其余功能与上面的工作队列差不多可自行测试
- linux 内核 工作队列
- Linux内核工作队列
- Linux内核:工作队列
- Linux 内核工作队列
- Linux内核:工作队列
- linux内核工作队列
- linux内核的工作队列
- tasklet、工作队列 - [linux内核]
- Linux内核机制:工作队列
- 论linux内核工作队列
- Linux内核中的工作队列
- 内核中工作队列(linux工作队列)
- 内核中工作队列(linux工作队列)
- 内核中工作队列(linux工作队列)
- linux内核分析之工作队列
- Linux内核中工作队列的操作
- Linux内核实践之工作队列
- linux内核知识之工作队列(workqueue)
- model中添加aar的解决方案
- 如何在VSCode中配置python环境
- Book
- 快速排序详解与实现
- react-事件绑定
- linux内核工作队列
- js中call和apply的应用和区别
- oracle对表空间进行操作
- deepmind_lab archlinux官网上面下载的----sudo-1.8.21.p2-1-x86_64.pkg.tar.xz---下载
- .Net网站架构设计(一)架构概念内容
- 时间的格局:让每一分钟为未来增值
- 分布函数
- Eclipse中出现SyntaxError: Non-UTF-8 code starting with '\xc4' in file错误
- .Net网站架构设计(二)Web服务器集群架构