电源管理的思考
一.涉及的通信机制介绍
熟悉之后可以跳过
1.inotify
Hotplug 是一种内核向用户态应用通报关于热插拔设备一些事件发生的机制,桌面系统能够利用它对设备进行有效的管理,udev 动态地维护 /dev 下的设备文件,inotify 是一种文件系统的变化通知机制,如文件增加、删除等事件可以立刻让用户态得知。
事实上,在 inotify 之前已经存在一种类似的机制叫 dnotify,但是它存在许多缺陷:
1. 对于想监视的每一个目录,用户都需要打开一个文件描述符,因此如果需要监视的目录较多,将导致打开许多文件描述符,特别是,如果被监视目录在移动介质上(如光盘和 USB 盘),将导致无法 umount 这些文件系统,因为使用 dnotify 的应用打开的文件描述符在使用该文件系统。
2. dnotify 是基于目录的,它只能得到目录变化事件,当然在目录内的文件的变化会影响到其所在目录从而引发目录变化事件,但是要想通过目录事件来得知哪个文件变化,需要缓存许多 stat 结构的数据。
3. Dnotify 的接口非常不友好,它使用 signal。
Inotify 是为替代 dnotify 而设计的,它克服了 dnotify 的缺陷,提供了更好用的,简洁而强大的文件变化通知机制:
1. Inotify 不需要对被监视的目标打开文件描述符,而且如果被监视目标在可移动介质上,那么在 umount 该介质上的文件系统后,被监视目标对应的 watch 将被自动删除,并且会产生一个 umount 事件。
2. Inotify 既可以监视文件,也可以监视目录。
3. Inotify 使用系统调用而非 SIGIO 来通知文件系统事件。
4. Inotify 使用文件描述符作为接口,因而可以使用通常的文件 I/O 操作select 和 poll 来监视文件系统的变化。(广泛应用于电源管理, udev中监控/dev下文件事件,tslib中提到通知触摸屏有触摸事件也通过此机制)
Inotify 可以监视的文件系统事件包括:
IN_ACCESS,即文件被访问
IN_MODIFY,文件被 write
IN_ATTRIB,文件属性被修改,如 chmod、chown、touch 等
IN_CLOSE_WRITE,可写文件被 close
IN_CLOSE_NOWRITE,不可写文件被 close
IN_OPEN,文件被 open
IN_MOVED_FROM,文件被移走,如 mv
IN_MOVED_TO,文件被移来,如 mv、cp
IN_CREATE,创建新文件
IN_DELETE,文件被删除,如 rm
IN_DELETE_SELF,自删除,即一个可执行文件在执行时删除自己
IN_MOVE_SELF,自移动,即一个可执行文件在执行时移动自己
IN_UNMOUNT,宿主文件系统被 umount
IN_CLOSE,文件被关闭,等同于(IN_CLOSE_WRITE | IN_CLOSE_NOWRITE)
IN_MOVE,文件被移动,等同于(IN_MOVED_FROM | IN_MOVED_TO)
注:上面所说的文件也包括目录。
用户态操作函数如下,
fd=Inotify_init()
Wd=Inotify_add_watch(fd,path,mask)//mask=IN_ALL_EVENTS
Inotify_rm_watch(fd,wd);
检测目录下或者文件的操作
调用read读取数据
Read(fd,buffer,MAX_BUF_SIZE)
Struct inotify_event{
__s32 wd;
__u32 mask; //具体位对应不同的文件或者目录的操作
__u32 cookie;//cookie to synchronize two events
__u32 len; //length (including nulls) of name
Char name[0]; //stub for possible name
}
Struct inotify_event *event;
Event=(struct inotify_event *)buffer;
2.socket
说起socket,在udev中也提到它的用途,不同的socket用途不一样,例如有NETLINK_KOBJECT_UEVENT专门用于监控uevent事件的socket,NetLINK_NETFILTER用于Netfilter,以及NETLINK_SELINUX SElinux event notifications等。
而在电源管理中用到的是最简单的AF_LOCAL族的socket,一般用于linux user app之间进行通信,socket的东东在网络上也很多,就不多说了,贴一些socket服务端一些简单代码,并且说明一个问题。
Struct sockaddr_un addr={AF_LOCAL, SERVER_SOCKET_NAME };
Int len=sizeof(addr.sun_famliy)+strlen(addr.sun_path);
Unlink(SERVR_SOCKET_NAME);
Fd=socket(AF_LOCAL,SOCK_STREAM,0);
Setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof(yes))
Bind(fd,&addr,len);
Chmod(addr.sun_path,0666)
Listen(fd,CLIENT_SIZE) //client_size 可以监听的client数目
Setsockopt SO_REUSEADDR
缺省条件下,一个套接口不能与一个已在使用中的本地地址捆绑(参见bind())。但有时会需要“重用”地址。因为每一个连接都由本地地址和远端地址的组合唯一确定,所以只要远端地址不同,两个套接口与一个地址捆绑并无大碍。为了通知WINDOWS套接口实现不要因为一个地址已被一个套接口使用就不让它与另一个套接口捆绑,应用程序可在bind()调用前先设置SO_REUSEADDR选项。请注意仅在bind()调用时该选项才被解释;故此无需(但也无害)将一个不会共用地址的套接口设置该选项,或者在bind()对这个或其他套接口无影响情况下设置或清除这一选项。
3.select poll
#include<poll.h>
Int poll(struct pollfd fds[],nfds_t nfds,int timeout)
对于Poll而言,每当调用Poll时,系统不会清空fds[]这个数组,特别在socket连接比较多的时候,在一定程度上提高处理效率,而select函数在调用之后,会清空它所检测的socket描述符集合,导致每次调用select之前都必须把socket描述符重新加入到待检测的集合中,因此,select函数适合于只检测一个socket描述符的情况,而Poll适用于大量socket描述符的情况。
如果需要反复使用select时,需要注意将需要监控的描述符重新加入到待检测的集合中(在写终端通信程序时 出过该问题)
Nfds为fds数组中fd的长
返回值:
>0:数组fds中准备好读、写或出错状态的那些socket描述符的总数量;
Pollfd结构
Struct pollfd
{
Int fd;//文件描述符
Short events;//等待的事件
Short revents;//实际发生的事件
}
POLLIN 有数据可以读入,read不会阻塞
二.总体设计思路
我们知道在/sys/文件系统中,提供了进入suspend模式的属性文件,向该属性文件写入“mem”即可进入睡眠模式,然而在用户层如何确定是否向该接口发送消息呢?
设计一个定时器,如果该定时器超时,则向属性文件写入“mem”,当然,我们在写属性文件时,应该能广播所有user app,通知其做好准备,系统resume时同样能广播所有的设备,
这样就需要一个服务和一个client或者多个client,client提供user app所需要的注册接口,将与电源管理有关的程序注册到client中来,并且提供相应的操作方法给应用程序调用。
三.Server
Server本身维护一个定时器,当定时器超时,就让系统进入节能模式。Server会接受其它进程的重置定时器的命令,阻止系统进入节能模式。
Server主要功能包括:
l当定时器超时,让系统进入节能模式。
l接受来至其它进程的连接注册(可能)。
l接收来至其它进程的重置定时器命令。
l接收来至其它进程的修改超时命令,维护超时时间在各个进程间同步。
l通知其它进程系统准备进入节能模式和离开节能模式事件。
在调用poll时,不仅包括上述的timer_fd,inotify_fd,socket_fd,reset_fd,还需要包括已注册
的所有client_fd,需要监控的evdev_fd(这里选择监控input设备中的keyboard)。
而socket_fd与client_fd数组相关,inotify_fd与evdev_fd数组相关。
每当/dev/input/eventX有输入时,则重置定制器。
在应用修改超时时间,事件需要广播(其他应用和电源管理器都需要知道此事件),使用inotify机制,应用和管理器都监听超时文件是否被修改,当发生修改之后,应用和管理器都重新读取超时文件的内容,更新超时时间。
整个server端的设计是比较简单的,就是全面的考虑电源管理所涉及的内容,然后对这些部分进行监控,如果有对应的事件就进行相应的处理,需要注意的是锁的使用和对事件的清除操作(一般通过读该文件清除)
思考:
1. 如何表明server已经运行? 通过一个lock文件来实现。当lock文件被锁则
server在运行,如果lock文件未被锁,则server未运行。
2. 对于server端,我们要考虑的问题就是定时器允许超时和不允许超时的情况,比
如在某些情况下是不允许进入睡眠模式的,这个时候需要通过client事件来实现。
对于系统休眠是所有注册的client同意的情况下才进入睡眠模式的。如果一个client不同意则永远不进入休眠。
设计一个定时器B(client端),区别于server的定时器A,运行定时器B和定时器A,当定时器B超时时(修改server和client共同访问的reset文件,reset_fd),则重置定时器A,保证定时器A不超时,从而不进入休眠。这样,我们把定时器A是否超时的控制权交给了client的定时器B,而定时器B是否超时(从而决定系统是否有可能进入休眠)由应用程序的状态决定的。
如果应用程序不允许进入省电,则设置工作状态为work,这样会启动定时器B,保证不进入睡眠。如果所有关联的应用程序均允许进入睡眠,则关闭定时器B,在满足条件的情况下,可以进入休眠。
是否允许启动定时器B,可以和思考1提到的部分一致,设置state文件,当该文件被锁时,启动定时器B,不允许进入休眠,否则反之。
四、client
1) 设计思路
对于上面提到的思考2,有了一个更清晰的认识,client端不仅仅是client的功能,还是一个库的概念。注册的应用程序通过调用client端提供的库函数,来实现对定时器B的操作。
在client端,维护一个线程(如果server没有运行,则这个线程也不运行)。
线程等待如下事件:
l 定时器B超时,将重置server端的定时器A,阻止系统进入节能模式。
l Server的定时器A超时时间被改变,将更新超时时间,如果当前状态为不允许进入休眠,还需要重启client的定时器B。
l 应用调用Register函数和server建立起新连接,则开始监听这个连接。
l Server通知用户,准备进入节能模式和退出节能模式,调用相应的回调函数。
整个client部分与server想关的功能还是比较简单的,关键在于库的注册函数和一些与各个应用的接口。
思考:
在新的应用register时,我们会通过eventfd,即threadwakeupfd通知socket fd更新自己的socket_fd,从而保证client线程中的socket fd是与server socket通道相bind的最新的socket fd.
这样就会存在一个问题,因为在server中,我们有fd_client数组用于记录bind到server 的所有socket,在广播resume和suspend事件时,可以通过fd_client数组逐一发送信号给远端地址,通知resume和suspend事件,而对于client端得socket_fd,只有一个,所以若发送失败则关闭对应的远端地址,从而只有最后一个注册到client中的应用的socket能收到server的信息。
也就是说最后一个socket需要通知所有的远端地址,已经收到resume或者suspend的通知,让他们调用自己的回调函数。故涉及到遍历通知信息的过程,所以设计注册函数时,需要注意链表的使用。(事实上大多的注册函数,多对一的情况下都需要考虑链表问题)
2) 库和接口函数
对于更上一层的应用程序而言,当其注册了自己的模块进入电源管理中之后,会调用一些接口来进行设置,那么我们该设置那些接口给应用层调用呢?(该部分在client中实现)
i)获取定时器A的超时时间
ii) 设置定时器A的超时时间(注意锁)
iii) 设置是否允许进入电源管理,是否开启定时器B(由state文件锁的占有情况决定,见server思考)
iv) 实现应用程序状态通知到server的方法,由状态的变化决定state文件
v)注册应用程序(链表),设置状态,如果状态为不允许进入休眠则调用库进行设置对应文件的操作(一般就电源NODE和下载的NODE)
以上提到的接口就是提供给应用的一些思路,而对应中间的库函数则是client端和接口
之间的一层细化操作,也应该得到注意。
3) 应用的状态切换
根据应用程序的工作状态(根据文件锁决定)决定定时器B的开关涉及到一个状态机的设计。
如果必要的情况下,会维护一个线程装用于检测状态,这个在电源供电框架中提到。所以可以封装一个接口函数,进行两部分初始化
1、对省电模式处理部分进行初始化,注册电源管理节点
2、如果是电池供电,则启动电源状态维护线程,以便即时更新状态信息,进行一些必要的操作。
Server为每个应用程序维护一个电源管理状态有三种状态
五.电源供电框架(kernel)与电源管理 (app)
事实上,我们还遗漏了一个问题,对于使用电源供电和电池供电的电源管理分析。在前面提到过,client会为维护一个线程,用于处理应用与server之间的工作,同时提供库和接口。而电源状态的变化这个过程除了人为的设置之外,还受所使用的电源的影响。
电源状态的监控是用uevent事件实现的。Linux内核中,供电部分的框架(drivers/power目录下,power_supply_core,power_supply_sysfs)提供了uevent机制,驱动可以在电源状态发生变化时发送uevent事件到用户空间,从而通知系统做相应的处理。(对于内核空间与用户空间的uevent通信,用的始终是NETLINK_KOBJECT_UEVENT这个socket通道)。
目前在下面这些情况下产生UEVENT事件:
(1)电池电量等级变化:电池电量等级包括满、高电量、普通电量、低电量、电量不足
(2)外电插入、拔出
(3)充电状态改变:包括充电中、禁止充电、不在充电、充饱等状态
所以,我们启动给一个电源状态维护线程,监控UEVENT事件发生,同时对不同的uevent事件调用不同的电源管理接口。比如在外电插入的情况下禁止进入休眠,在电池低电量的时候允许进入休眠等设置。
六.一些细节
1. 以前提到过系统调用的用法,在电源管理中定时器也用到了系统调用,下载timerfd.patch,实现部分如下,
#if defined(__arm__)
#define __NR_timerfd_create (__NR_SYSCALL_BASE+350)
#define __NR_timerfd_settime (__NR_SYSCALL_BASE+353)
#define __NR_timerfd_gettime (__NR_SYSCALL_BASE+354)
#define TFD_TIMER_ABSTIME (1 << 0)
#endif
//#endif /* __NR_timerfd_create */
static int timerfd_create(int clockid, int flags)
{
return syscall(__NR_timerfd_create, clockid, flags);
}
static int timerfd_settime(int ufc, int flags, const struct itimerspec *utmr, struct itimerspec *otmr)
{
return syscall(__NR_timerfd_settime, ufc, flags, utmr, otmr);
}
2. __attribute__ ((constructor))
Gcc为函数提供了几种类型的属性,其中有构造函数constructor和析构函数destructors
Static void start(void) __attribute__ ((constructor))
Static void stop(void) __attribute__ ((destructors))
带有构造函数属性的函数会在main函数之前执行,而析构函数会在_after_main()退出时执行,具体的问题分析可以参见
http://chongsoft.bokee.com/5738209.html
__attribute__ ((__format__(__printf__,m,n)))
This指针,继承概念
3. 在client中涉及到threadWakeup_fd,因为它是创建的一个事件fd,用于socket更新时,通知系统重新poll,所以比较简单
调用threadWakeup_fd=eventfd(0);
Fcntl(threadWakeup_fd,F_SETFD,FD_CLOEXEC);
当需要通知系统重新poll时,只要向该fd中写入一些数据,原来的poll会有事件上报,在该处理函数中,通知重新poll。
4. 关于用户空间定时器的一些操作
内核空间的定时器的使用在linux内核一些机制中提到过。
这里说的用户空间的定时器的操作,到最终的操作肯定为内核定时器。
Timer_fd=timerfd_create(CLOCK_MONOTONIC,0); //
Timerfd_settime(timer_fd,0,&tmr,NULL) //启动定时器
定时时间在struct itimerspec tmr结构中,这个定时器不像内核定时器,可以在时间到时调用定时处理函数,所以我们需要将此fd加入select或者poll中,从而在时间超时时做相应的处理。
5. static int close_socket(int fd)
{
Shutdown(fd,SHUT_RDWR);
Return close(fd);
}
读取一个文件有多少数据可读
Ioctl(fd,FIONREAD,&ibytestoread);