Android8.0.0-r4的Binder进程间通信机制_android service binder 支持并发吗-程序员宅基地

技术标签: Android代码经验  

    Android8.0.0-r4的Binder进程间通信机制

    Binder是Android系统中进程间通讯(IPC)的一种方式,也是Android系统中最重要的特性之一。Android中的四大组件Activity,Service,Broadcast,ContentProvider,不同的App等都运行在不同的进程中,它是这些进程间通讯的桥梁。正如其名“粘合剂”一样,它把系统中各个组件粘合到了一起,是各个组件的桥梁

1. Binder 架构


    

    Binder 通信采用 C/S 架构,从组件视角来说,包含 Client、 Server、 ServiceManager 以及 Binder 驱动,其中 ServiceManager 用于管理系统中的各种服务。
    Binder 在 framework 层进行了封装,通过 JNI 技术调用 Native(C/C++)层的 Binder 架构。

    Binder 在 Native 层以 ioctl 的方式与 Binder 驱动通讯。


2. Binder 机制


    首先需要注册服务端,只有注册了服务端,客户端才有通讯的目标,服务端通过 ServiceManager 注册服务,注册的过程就是向 Binder 驱动的全局链表 binder_procs 中插入服务端的信息(binder_proc 结构体,每个 binder_proc 结构体中都有 todo 任务队列),然后向 ServiceManager 的 svcinfo 列表中缓存一下注册的服务。
    有了服务端,客户端就可以跟服务端通讯了,通讯之前需要先获取到服务,拿到服务的代理,也可以理解为引用。比如下面的代码:
//获取WindowManager服务引用

WindowManager wm = (WindowManager)getSystemService(getApplication().WINDOW_SERVICE);

//获取PowerManager服务引用

owerManager mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);

    获取服务端的方式就是通过 ServiceManager 向 svcinfo 列表中查询一下返回服务端的代理,svcinfo 列表就是所有已注册服务的通讯录,保存了所有注册的服务信息

    有了服务端的引用就可以向服务端发送请求了,通过 BinderProxy 将请求参数发送给 ServiceManager,通过共享内存的方式使用内核方法 copy_from_user() 将参数先拷贝到内核空间,这时客户端进入等待状态,然后 Binder 驱动向服务端的 todo 队列里面插入一条事务,执行完之后把执行结果通过 copy_to_user() 将内核的结果拷贝到用户空间(这里只是执行了拷贝命令,并没有拷贝数据,binder只进行一次拷贝),唤醒等待的客户端并把结果响应回来,这样就完成了一次通讯。


3、Binder内核驱动程序

用户空间/内核空间
     Kernel space 是 Linux 内核的运行空间,可以执行任意命令,调用系统的一切资源。
   User space是用户程序的运行空间,只能执行简单的运算,不能直接调用系统资源,必须通过系统接口(又称 system call),才能向内核发出指令。
    用户空间中 binder_open(), binder_mmap(), binder_ioctl() 这些方法通过 system call 来调用内核空间 Binder 驱动中的方法。内核空间与用户空间共享内存通过 copy_from_user(), copy_to_user() 内核方法来完成用户空间与内核空间内存的数据传输。 Binder驱动中有一个全局的 binder_procs 链表保存了服务端的进程信息。
    为了安全,用户空间/内核空间是隔离的,即使用户的程序崩溃了,内核也不受影响。

3.1 Binder内核驱动程序

    Binder内核驱动程序初始化:


Binder初始化做的事情就是在设备上创建了一个目录,一个文件

3.1.1 /proc/binder/proc目录 

    每一个使用了Binder进程间通信机制的进程在该目录下都对应有一个文件,这些文件以进程ID来命名,通过他们就可以读取到各个进程的Binder线程池、Binder实体对象、Binder引用对象以及内核缓冲区的信息

3.1.2 /dev/binder设备文件 
    这个设备文件的操作方法列表是由全局变量binder_fops指定的,在设备文件里有操作方法:
        打开方法binder_open
        内存映射binder_mmap
        IO控制binder_ioctl

3.1.2.1 打开方法binder_open

    1、一个进程在使用Binder进程间通信机制之前,首先就会调用binder_open来打开设备文件/dev/binder来获得一个文件描述,然后才能通过这个文件描述和Binder驱动程序交互,继而执行Binder进程间通信。
    2、Binder驱动程序会在内核中创建一个binder_proc结构体proc。结构体大家当对象理解就好了。并将proc加入到一个hash队列binder_procs中,Binder驱动程序将所有打开了设备文件/dev/binder的进程都加入到全局hash队列binder_procs中,通过遍历这个hash队列就知道系统当前有多少个进程在使用Binder进程间通信机制。

    3、文件描述和结构体proc关联在一起,使用文件描述作为参数调用mmap和ioctl,内核就能找到proc结构体。


3.1.2.2 内存映射binder_mmap

    1、进程打开了设备文件/dev/binder之后,还必须要调用函数mmap把这个设备文件映射到进程的地址空间,映射到进程的地址空间,映射到进程的地址空间。

    2、为进程分配内核缓冲区,以便用来传输进程间通信的数据。

    内核缓冲区:当进程调用函数mmap将设备文件/dev/binder映射到自己的地址空间时,就会分配内核缓冲区,Binder驱动程序为进程分配的内核缓冲区有两个地址,其中一个是用户空间地址,由参数vma所指向的一个vm_area_struct结构体来描述;另一个地址是内核空间地址,由变量area所指向的一个vm_struct结构体来描述。进程通过用户空间地址来访问这块内存缓冲区的内容,而Binder驱动程序通过内核空间地址来访问这块内核缓冲区内容,由于它们是连续的,并且起始值相差一个固定值,因此,只要知道其中一个地址,就可以方便地计算另外一个地址。

    3、将内核缓冲区的信息和binder_proc关联在一起


3.1.2.3 系统调用IO控制binder_ioctl

    1、系统调用:进程可以通过系统调用访问内核。

    2、IO命令控制:进程与内核间的很多协议通信传输就是依靠这个方法的case来进行相应的处理


4、Binder进程间通信库

    Android系统在应用程序框架层中将各种Binder驱动程序操作封装成一个Binder库,这样进程就可以方便地调用Binder库提供的接口来实现进程间通信。Android提供了很多的方法,其中基于Binder原理实现的进程间通信方式有Content Provider、Messenger、AIDL等,其中Messenger是基于AIDL的封装,Content Provider和AIDL底层都是Binder。

模板类BnIterface
    模板类BnIterface继承自BBinder类,BBinder类有两个重要的成员函数transact和OnTransact,当一个Binder代理对象通过Binder驱动程序向一个Binder本地对象发出一个进程间通信请求时,Binder驱动程序就会调用Binder本地对象的成员函数transact来处理该请求。OnTransact是由BBinder的子类来实现的,它就是负责分发和业务相关的进程间通信请求,比如你要提供什么服务,服务里想做些什么,就是在这里写逻辑的。BBinder类又继承了IBinder类,而后者有继承了RefBase类。

模板类BpInterface
    模板类BpInterface继承自BpRefBase类,BprefBase类也继承了RefBase类,BpRefBase类有一个重要的成员变量mRemote,它指向一个BpBinder对象(Binder代理对象),可以用通过成员函数remote来获取。

BpBinder类
    BpBinder实现了BpRefBase类的进程间通信接口,BpBinder成员变量mHandle是一个整数,表示一个Client组件的句柄值,每一个Client组件在Binder驱动程序中都对应一个Binder引用对象,而每一个Binder引用对象都有一个句柄值,这里又出现一个句柄值概念,句柄值是和内存地址息息相关的,通过句柄值就可以在列表中找到Binder引用对象。其中Client组件就是通过这个句柄值来和Binder驱动程序中的Binder引用对象建立对应关系的。BpBinder类成员函数transact用来发送进程间通信请求。

Binder线程池
    每一个使用了Binder进程间通信机制的进程都有一个Binder线程池们用来处理进程间通信请求,这个Binder线程池由Binder驱动程序来维护,还记的之前打开/dev/binder设备文件创建的结构体binder_proc吗,它的成员变量threads就是一个红黑树的根节点,它以线程ID作为关键字来组织一个进程的Binder线程池。进程可以调用ioctl将一个线程注册到Binder驱动中。

IPCThreadState类
    无论是BBinder类(Binder本地对象),还是BpBinder类(Binder代理对象),它们都是通过IPCThreadState类来和Binder驱动程序交互的,对于每一个Binder线程来说,它内部都有一个IPCThreadState对象,IPCThreadState对象我们可以IPCThreadState的静态成员函数self来获取,并且调用它的成员函数transact(臭不要脸起一样的名字)来和Binder驱动程序交互,在IPCThreadState类的成员函数transact内部,与Binder驱动程序的交互操作又是通过调用成员函数talkWithDriver来实现的,OK终于知道进程是怎么和内核中Binder驱动程序交互的。

ProcessState类
    IPCThreadState类有一个成员变量mProcess,它指向一个ProcessState对象。对于每一个使用了Binder进程间通信机制的进程来说,它内部都有一个ProcessState对象,它负责初始化设备,即打开/dev/binder设备文件,以及将设备文件/dev/binder映射到进程的地址空间。OK我们又找到了打开设备文件和进行内存映射的类。ProcessState对象在进程范围内是唯一的,不像IPCThreadState对象每个Binder线程内部都有一个,因此每个Binder线程都可以通过它来和Binder驱动程序建立连接。

    Service组件(Server端)和Client组件(Client端)分别使用模板类BnIterface和BpInterface来描述,Bn是Binder native的缩写,Bp是Binder Proxy的缩写,前者称为Binder本地对象,后者称为Binder代理对象,实际上进程间通信就是通过Binder代理对象发送进程间通信请求,通过Binder驱动等一系列操作,最后由Binder本地对象来处理请求,完成进程间通信。在使用Binder库开发Service组件和Client组件时,除了要定义Service组件接口之外,还必须要实现一个Binder本地对象和一个Binder代理对象,而如何获取Binder代理对象或者注册一个Binder本地对象就需要Service Manager的协助


    Binder本地对象、Binder代理对象都是在用户空间中的,也就是C/S端进程中,也说了实际上进程间通信就是通过Binder代理对象发送进程间通信请求,通过Binder驱动等一系列操作,最后由Binder本地对象来处理请求,完成进程间通信。

    Binder引用对象、Binder实体对象都是位于Binder驱动程序中的其实就是C语言写的数据结构,Binder代理对象对应的就是Binder引用对象,Binder本地对象对应的就是Binder实体对象,而Binder引用对象,引用的就是Binder实体对象,Binder实体对象中有Binder引用对象的列表,为什么是对应列表呢?因为Binder进程间通信是支持并发的,同时一个Server端是可以向多个Client端提供服务的,所以用一个列表来保存Client端Binder代理对象对应的Binder本地对象,Binder引用对象有又相应的句柄值。


5、Binder 进程与线程

    对于底层Binder驱动,通过 binder_procs 链表记录所有创建的 binder_proc 结构体,binder 驱动层的每一个 binder_proc 结构体都与用户空间的一个用于 binder 通信的进程一一对应,且每个进程有且只有一个 ProcessState 对象,这是通过单例模式来保证的。在每个进程中可以有很多个线程,每个线程对应一个 IPCThreadState 对象,IPCThreadState 对象也是单例模式,即一个线程对应一个 IPCThreadState 对象,在 Binder 驱动层也有与之相对应的结构,那就是 Binder_thread 结构体。在 binder_proc 结构体中通过成员变量 rb_root threads,来记录当前进程内所有的 binder_thread。

    Binder 线程池:每个 Server 进程在启动时创建一个 binder 线程池,并向其中注册一个 Binder 线程;之后 Server 进程也可以向 binder 线程池注册新的线程,或者 Binder 驱动在探测到没有空闲 binder 线程时主动向 Server 进程注册新的的 binder 线程。对于一个 Server 进程有一个最大 Binder 线程数限制,默认为16个 binder 线程,例如 Android 的 system_server 进程就存在16个线程。对于所有 Client 端进程的 binder 请求都是交由 Server 端进程的 binder 线程来处理的。

6、Service Manager上下文管理者

     ServicerManager是Binder进程间通信机制的核心组件之一,它扮演着Binder进程间通信机制上下文管理者的角色,同时负责管理系统中Service组件,并向Client组件提供获取Service代理对象的服务。ServiceManager 的作用很简单就是提供了查询服务和注册服务的功能。
    ServiceManager 分为 framework 层和 native 层,framework 层只是对 native 层进行了封装方便调用,图上展示的是 native 层的 ServiceManager 启动过程。
    ServiceManager 是由init进程负责启动的,init进程是Linux的第一个进程,init 进程解析 init.rc 文件调用 service_manager.c 中的 main() 方法入口启动的。native 层有一个 binder.c 封装了一些与 Binder 驱动交互的方法。


ServiceManager 的启动分为三步,首先打开驱动创建全局链表 binder_procs,然后将自己当前进程信息保存到 binder_procs 链表,最后开启 loop 不断的处理共享内存中的数据,并处理 BR_xxx 命令(ioctl 的命令,BR 可以理解为 binder reply 驱动处理完的响应)

6.1 打开/dev/binder设备文件、映射到本进程的地址空间

    打开/dev/binder设备文件、映射到本进程的地址空间,获得文件描述用来和驱动程序交互用,Binder驱动程序分配内核缓冲区,创建binder_proc结构体等。

6.2 注册成为管理者

    注册成为管理者,Service Manager想注册成为管理者,就必须要通过IO控制命令将自己注册到Binder驱动程序,IO控制命令可以理解成进程和内核的交互方法,既然Service Manager是一个特殊的Server端,如果想成为Server那么它就需要有一个自己Binder本地对象,我们知道创建对象在内存中都是有一个对应的内存地址值的,但是Service Manager的Binder本地对象是一个虚拟对象,它的地址值为0。而Binder驱动程序就是通过binder_ioctl函数来处理IO控制命令的。简单的分析下注册流程:

    首先,第一步为ServiceManager创建的结构体binder_proc:

    binder_proc中的threads是对应这Binder线程池的(threads红黑树,以线程PID为关键字来组织),Binder线程池中的线程就是处理和Binder驱动程序交互的,而描述这个Binder线程的结构体就是binder_thread,Binder驱动程序会查看这个threads有没有没对应的binder_thread结构体,因为ServiceManager是进行初始化注册,所以没有对应的结构体,Binder驱动程序就为Service Manager创建一个这样的结构体,当前线程就是Service Manager的主线程。

    其次,Binder驱动程序会根据ServiceManager的Binder本地对象(地址为0的虚拟对象),判读是否存在对应的Binder实体对象,还是因为进行初始化注册,没有对应的实体对象,就为其建一个Binder实体对象,保存在全局变量binder_context_mgr_node中。
    最后,将这个新创建的实体对象加入结构体binder_proc的nodes成员变量里,然后继续返回用户空间,开始开启循环。

    总结一下就是,让驱动程序根据IO控制命令弄出Binder线程,Binder实体对象,然后和binder_proc这个结构体的相关成员变量关联起来。

6.3 循环等待Client进程请求

    出现一个新的结构体binder_writr_read,binder_writr_read就包含输入缓存区和输出缓冲区,循环依然使用的是IO控制命令通过Binder线程(binder_thread结构体)以及inder_writr_read来和Binder驱动程序交互,依然使用的是binder_ioctl函数来处理IO控制命令,注册的时候我们已经创建了binder_thread结构体,所有就使用这个结构体,使用copy_from_use将从用户空间(Service Manager)传进来的结构体binder_writr_read拷贝进来。既然是交互,那么就有接收和返回,当进程同过IO控制命令传递binder_writr_read数据的时候,如何判断是让Binder驱动程序读数据,还是让Binder驱动程序写入数据返回给用户空间呢,这里就是通过判断binder_writr_read结构体的缓冲区,如果输入缓存区长度大于0,Binder驱动程序就使用binder_thread_write来处理数据,如果输出缓存区长度大于0,Binder驱动程序就使用binder_thread_read将需要处理的工作项写入到输出缓冲区,即将相应的返回协议写入该缓冲区中,以便进程在用户空间处理。工作项在内核中使用binder_transaction结构体来描述,工作项对应的数据就是binder_transaction_data结构。
    总结,循环我们可以想象我们工作中使用的轮询操作,就是不停的发送数据给Binder驱动程序,然后驱动程序会根据发送来的数据状态是将数据copy下来,还是去找看看有没有你需要做的工作返回给你做,总之一句话就是循环等待Client进程请求。


7、ServiceManager代理对象的获取

    ServiceManager成为管理者了,也开启循环了,它已经准备好的一切,就等为Client进程服务了。要想使用其他进程提供的服务,就需要拿到Server端的Binder代理对象,要使用ServiceManager的服务,当然也要获取它Binder代理对象,然后通过代理对象找到Binder引用对象然后找到Binder实体对象,然后最终交给Binder本地对象来处理。但是Service Manager的代理对象比较特殊,它省去了和Binder内核驱动的交互,提供了直接获取ServiceManager的Binder代理对象的函数defaultServiceManager。
    进程通过使用Binder库的defaultServiceManager函数获取ServiceManager的过程也比较简单,
        1.判断进程中有没有ServiceManager代理对象,有就直接用还获取这个代理对象,如果没有继续。

       2.这个部分总共分为3小部分,获取Service Manager代理对象的过程,实际上就是进程间通信的过程,Client端和Service Manager进程间通信,凡是需要进程间通信的,都需要打开和映射/dev/binder设备文件这个过程,从上文知道干这件事的类就是ProcessState对象,然后在使用句柄值0创建一个Binder代理对象,接着将这个Binder代理对象调用模板函数封装成一个Service Manager代理对象。


对于一般的Service组件来说,Client进程首先要通过Binder驱动程序来获得它的一个句柄值,然后根据这个句柄值创建Binder代理对象,最后将这个Binder对象封装成一个实现特定接口的代理对象。因为ServiceManager的句柄值恒为0,所以省略了和Binder驱动程序的交互过程。对于非Servicer Manager来说,获取句柄值就需要和内核进行交互。

7.1 Server端,服务的提供者

    服务不是你想提,想提就能提,要想成为Server端,先得将Service组件注册到ServiceManager中,接着启动Binder线程池来等待和处理Client进程的通信请求。 

    注册服务就是Binder库提供的addService函数。首先如果想成为Server端,就得先准备Binder本地对象,数据,Service名称,偏移数组(Binder本地对象的地址值)等,将这部分东西通过一个Parcel的对象进行多次封装,使用Service Manager代理对象将Parcel封装的数据(最终转换成flat_binder_object结构体)发送给Binder内核驱动程序,内核驱动程序就会根据ServiceManager代理对象的句柄值0,找到了全局变量binder_context_mgr_node,最后定位到了binder_proc结构体和内核缓冲区,将数据拷贝到ServiceManager对应的内核缓冲区处理数据,同时也可以根据ServiceManager代理对象传入的数据分析出注册Service的进程对应的binder_proc结构体,由于是首次注册该binder_proc结构体的nodes对象一定为null,Binder驱动程序就会为创建一个Binder实体对象关联到nodes,然后再看它有没有引用对象,肯定没有啊实体都没有,哪里来的引用,接着创建引用对象(binder_ref),然后将这个对象(binder_ref)插入到Service Manager进程的binder_proc对象的一个链表中。最后启动Binder线程池将当前线程加入到Binder线程池中。


7.2 Client端,服务的请求者

    Client端想要使用服务,当然需要获取Server端的代理对象了,只有获取到这个代理对象,才能通过代理对象来让Server端的本地对象服务。Server端的代理对象的换取关键就是句柄值的获取,Service Manager代理对象的句柄值恒为0,直接用0去获取。


    Server端的Binder引用对象(desc变量就是分配的句柄值)最终通过一系列的交互插入到ServiceManager的svclist列表中,所以Client端想要获取Server端的代理对象就需要去ServiceManager中去找,而去Service Manager找的过程又是一个进程间通信过程,还是要获得Service Manager的代理对象。

    Binder进程间通信库提供了defaultServiceManager()函数来获取ServiceManager的代理对象,提供了addServicer()函数来注册成为Server端,那么也同时提供了一个函数getService()来获取Server端的代理对象,在调用getService()来获取Server端的代理对象时,需要指定这个Service组件的名称,内核驱动最终联系到Service Manager进程,然后ServiceManager进程根据名称数据,在和内核交互,从全局变量svclist中查找,找到指定的Server端句柄,内核根据句柄找到相应的引用对象,然后在根据引用对象找到实体对象,重新创建一个引用对象供客户端转换成代理对象


8. ServiceManager 注册服务


    注册 MediaPlayerService 服务端,通过 ServiceManager 的 addService() 方法来注册服务。
    首先 ServiceManager 向 Binder 驱动发送 BC_TRANSACTION 命令(ioctl 的命令,BC 可以理解为 binder client 客户端发过来的请求命令)携带 ADD_SERVICE_TRANSACTION 命令,同时注册服务的线程进入等待状态 waitForResponse()。 Binder 驱动收到请求命令向 ServiceManager 的 todo 队列里面添加一条注册服务的事务。事务的任务就是创建服务端进程 binder_node 信息并插入到 binder_procs 链表中。

    事务处理完之后发送 BR_TRANSACTION 命令,ServiceManager 收到命令后向 svcinfo 列表中添加已经注册的服务。最后发送 BR_REPLY 命令唤醒等待的线程,通知注册成功。


9. ServiceManager 获取服务


    获取服务的过程与注册类似,相反的过程。通过 ServiceManager 的 getService() 方法来注册服务。
    首先 ServiceManager 向 Binder 驱动发送 BC_TRANSACTION 命令携带 CHECK_SERVICE_TRANSACTION 命令,同时获取服务的线程进入等待状态 waitForResponse()。

    Binder 驱动收到请求命令向 ServiceManager 的发送 BC_TRANSACTION 查询已注册的服务,查询到直接响应 BR_REPLY 唤醒等待的线程。若查询不到将与 binder_procs 链表中的服务进行一次通讯再响应。


10. 一次完整通讯


    在使用 Binder 时基本都是调用 framework 层封装好的方法,AIDL 就是 framework 层提供的傻瓜式是使用方式。假设服务已经注册完,客户端怎么执行服务端的方法:
    首先通过 ServiceManager 获取到服务端的 BinderProxy 代理对象,通过调用 BinderProxy 将参数,方法标识(例如:TRANSACTION_test,AIDL中自动生成)传给 ServiceManager,同时客户端线程进入等待状态。
    ServiceManager 将用户空间的参数等请求数据复制到内核空间,并向服务端插入一条执行执行方法的事务。事务执行完通知 ServiceManager 将执行结果从内核空间复制到用户空间,并唤醒等待的线程,响应结果,通讯结束


参考:

https://blog.csdn.net/freekiteyu/article/details/70082302
https://blog.csdn.net/omyrobin/article/details/78832654
https://blog.csdn.net/ma969070578/article/details/45871323/

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/nwpushuai/article/details/79662155

智能推荐

已知num为无符号十进制整数,请写一非递归算法,该算法输出num对应的r进制的各位数字。要求算法中用到的栈采用线性链表存储结构(1<r<10)。-程序员宅基地

文章浏览阅读74次。思路:num%r得到末位r进制数,num/r得到num去掉末位r进制数后的数字。得到的末位r进制数采用头插法插入链表中,更新num的值,循环计算,直到num为0,最后输出链表。//重置,s指针与头指针指向同一处。//更新num的值,至num为0退出循环。//末位r进制数存入s数据域中。//头插法插入链表中(无头结点)//定义头指针为空,s指针。= NULL) //s不为空,输出链表,栈先入后出。

开始报名!CW32开发者扶持计划正式进行,将助力中国的大学教育及人才培养_cw32开发者扶持计划申请-程序员宅基地

文章浏览阅读176次。武汉芯源半导体积极参与推动中国的大学教育改革以及注重电子行业的人才培养,建立以企业为主体、市场为导向、产学研深度融合的技术创新体系。2023年3月,武汉芯源半导体开发者扶持计划正式开始进行,以打造更为丰富的CW32生态社区。_cw32开发者扶持计划申请

希捷硬盘开机不识别,进入系统后自动扫描硬件以识别显示_st2000dm001不认盘-程序员宅基地

文章浏览阅读5.7k次。2014年底买的一块2TB希捷机械硬盘ST2000DM001-1ER164,用了两年更换了主板、CPU等,后来出现开机不识别的情况,具体表现为:关机后开机,找不到硬盘,就进入BIOS了,只要在BIOS状态下待机半分钟左右再重启,硬盘就会出现。进入系统后,重启(这个过程中主板对硬盘始终处于供电状态),也不会出现不识别硬盘的现象。就好像是硬盘或主板上某个电容坏了一样,刚开始给硬盘通电的N秒钟内电容未能..._st2000dm001不认盘

ADO.NET包含主要对象以及其作用-程序员宅基地

文章浏览阅读1.5k次。ADO.NET的数据源不单单是DB,也可以是XML、ExcelADO.NET连接数据源有两种交互模式:连接模式和断开模式两个对应的组件:数据提供程序(数据提供者)&DataSetSqlConnectionStringBuilder——连接字符串Connection对象用于开启程序和数据库之间的连接public SqlConnection c..._列举ado.net在操作数据库时,常用的对象及作用

Android 自定义对话框不能铺满全屏_android dialog宽度不铺满-程序员宅基地

文章浏览阅读113次。【代码】Android 自定义对话框不能铺满全屏。_android dialog宽度不铺满

Redis的主从集群与哨兵模式_redis的主从和哨兵集群-程序员宅基地

文章浏览阅读331次。Redis的主从集群与哨兵模式Redis的主从模式全量同步增量同步Redis主从同步策略流程redis主从部署环境哨兵模式原理哨兵模式概述哨兵模式的作用哨兵模式项目部署Redis的主从模式1、Redis虽然读取写入的速度都特别快,但是也会产生读压力特别大的情况。2、为了分担读压力,Redis支持主从复制,保证主数据库的数据内容和从数据库的内容完全一致。3、Redis的主从结构可以采用一主多从或者级联结构,Redis主从复制可以根据是否是全量分为全量同步和增量同步。全量同步Redis全量复制一般发_redis的主从和哨兵集群

随便推点

mysql utf-8的作用_为什么不建议在MySQL中使用UTF-8-程序员宅基地

文章浏览阅读116次。作者:brightwang原文:https://www.jianshu.com/p/ab9aa8d4df7d最近我遇到了一个bug,我试着通过Rails在以“utf8”编码的MariaDB中保存一个UTF-8字符串,然后出现了一个离奇的错误:Incorrect string value: ‘😃 我用的是UTF-8编码的客户端,服务器也是UTF-8编码的,数据库也是,就连要保存的这个字符串“????..._mysql utf8的作用

MATLAB中对多张图片进行对比画图操作(包括RGB直方图、高斯+USM锐化后的图、HSV空间分量图及均衡化后的图)_matlab图像比较-程序员宅基地

文章浏览阅读278次。毕业这么久了,最近闲来准备把毕设过程中的代码整理公开一下,所有代码其实都是网上找的,但都是经过调试能跑通的,希望对需要的人有用。PS:里边很多注释不讲什么意思了,能看懂的自然能看懂。_matlab图像比较

16.libgdx根据配置文件生成布局(未完)-程序员宅基地

文章浏览阅读73次。思路:  screen分为普通和复杂两种,普通的功能大部分是页面跳转以及简单的crud数据,复杂的单独弄出来  跳转普通的screen,直接根据配置文件调整设置<layouts> <loyout screenId="0" bg="bg_start" name="start" defaultWinId="" bgm="" remark=""> ..._libgdx ui 布局

playwright-python 处理Text input、Checkboxs 和 radio buttons(三)_playwright checkbox-程序员宅基地

文章浏览阅读3k次,点赞2次,收藏13次。playwright-python 处理Text input和Checkboxs 和 radio buttonsText input输入框输入元素,直接用fill方法即可,支持 ,,[contenteditable] 和<label>这些标签,如下代码:page.fill('#name', 'Peter');# 日期输入page.fill('#date', '2020-02-02')# 时间输入page.fill('#time', '13-15')# 本地日期时间输入p_playwright checkbox

windows10使用Cygwin64安装PHP Swoole扩展_win10 php 安装swoole-程序员宅基地

文章浏览阅读596次,点赞5次,收藏6次。这是我看到最最详细的安装说明文章了,必须要给赞!学习了,也配置了,成功的一批!真不知道还有什么可补充的了,在此做个推广,喜欢的小伙伴,走起!_win10 php 安装swoole

angular2里引入flexible.js(rem的布局)_angular 使用rem-程序员宅基地

文章浏览阅读1k次。今天想实现页面的自适应,本来用的是栅格,但效果不理想,就想起了rem布局。以前使用rem布局,都是在原生html里,还没在框架里使用过,百度没百度出来,就自己琢磨,不知道方法规范不规范,反正成功了,操作如下:1、下载flexible.js2、引入到angular项目里3、根据自己的需要修改细节3.1、在flexible.js里修改每份的像素,3.2、引入cssrem插件,在设置里设..._angular 使用rem

推荐文章

热门文章

相关标签