<Android开发> Android vold - 第四篇 vold 的NetlinkHandler类简介

article/2025/10/2 18:08:12

本系列主要介绍 Android vold,分为以下篇章
<Android开发> Android vold - 第一篇 vold前言简介
<Android开发> Android vold - 第二篇 vold 的main()函数简介
<Android开发> Android vold - 第三篇 vold 的NetLinkManager类简介
<Android开发> Android vold - 第四篇 vold 的NetlinkHandler类简介

继第三篇vold 的NetLinkManager类简介后,我们来看看NetlinkHandler类的内容。

6 NetlinkHandler类

在第三篇介绍NetLinkManager类是,提到了 NetLinkManager.start()函数,在该函数的:
第33行:mHandler = new NetlinkHandler(mSock);,给mHandler 申请对象;
第34行:mHandler->start() ,启动监听内核uevent事件。
由此进入 NetlinkHandler类方法中。

6.1 NetlinkHandler类定义
NetlinkHandler类定义内容如下:

路径:LINUX/android/system/vold/NetlinkHandler.h
class NetlinkHandler: public NetlinkListener {
public:explicit NetlinkHandler(int listenerSocket);virtual ~NetlinkHandler();int start(void);int stop(void);
protected:virtual void onEvent(NetlinkEvent *evt);
};

该类继承自父类NetlinkListener;NetlinkHandler类实现了start、stop、onEvent三个方法,以及构造和析构函数。

路径:LINUX/android/system/vold/NetlinkHandler.cpp
NetlinkHandler::NetlinkHandler(int listenerSocket) :NetlinkListener(listenerSocket) {
}NetlinkHandler::~NetlinkHandler() {
}int NetlinkHandler::start() {return this->startListener();   //NetlinkHandler的start函数调用的startListener函数正是 NetlinkListener 的父类 SocketListener 类中的实现
}int NetlinkHandler::stop() {return this->stopListener();
}
// 获取到 kernel 事件 后调用这个函数进行处理
void NetlinkHandler::onEvent(NetlinkEvent *evt) {VolumeManager *vm = VolumeManager::Instance();const char *subsys = evt->getSubsystem();if (!subsys) {SLOGW("No subsystem found in netlink event");return;}if (!strcmp(subsys, "block")) {vm->handleBlockEvent(evt);}
}

可以看到,构造函数将new NetlinkHandler时传入的socket直接传给了父类NetlinkListener,所以socket是NetlinkListener类里使用的,后面会继续看该类。
start函数也是直接调用了父类NetlinkListener的start函数。
stop函数也是直接调用了父类NetlinkListener的stop函数。
onEvent函数是虚函数,父类NetlinkListener没有实现,所以NetlinkHandler类自己实现该方法。

6.2 new NetlinkHandler类

从6.1节可知构造函数将new NetlinkHandler时传入的socket直接传给了父类NetlinkListener,所以socket是NetlinkListener类里使用的。

6.3 NetlinkListener 类定义
看看NetlinkHandler的父类NetlinkListener的定义

路径:LINUX/android/system/core/libsysutils/include/sysutils/NetlinkListener.h
class NetlinkEvent;class NetlinkListener : public SocketListener {char mBuffer[64 * 1024] __attribute__((aligned(4)));int mFormat;public:static const int NETLINK_FORMAT_ASCII = 0;static const int NETLINK_FORMAT_BINARY = 1;static const int NETLINK_FORMAT_BINARY_UNICAST = 2;#if 1/* temporary version until we can get Motorola to update their* ril.so.  Their prebuilt ril.so is using this private class* so changing the NetlinkListener() constructor breaks their ril.*/NetlinkListener(int socket);NetlinkListener(int socket, int format);
#elseNetlinkListener(int socket, int format = NETLINK_FORMAT_ASCII);
#endifvirtual ~NetlinkListener() {}protected:virtual bool onDataAvailable(SocketClient *cli);virtual void onEvent(NetlinkEvent *evt) = 0;
};

首先我们又看到 NetlinkListener类又进一步集成其父类SocketListener;
以及定义了一些变量,暂不解释变量,用到在说明;
实现了NetlinkListener的两种方法;还有虚函数onEvent,在NetlinkListener的子类NetlinkHandler中实现了。虚函数onDataAvailable则是继承自其父类SocketListener,并实现。

路径:LINUX/android/system/core/libsysutils/src/NetlinkListener.cpp
NetlinkListener::NetlinkListener(int socket) :SocketListener(socket, false) {mFormat = NETLINK_FORMAT_ASCII;
}

NetlinkListener类的构造函数只是初始化了编码类型,然后又进入其父类SocketListener了。

6.4 SocketListener类定义
看看NetlinkListener的父类SocketListener的定义;

路径:LINUX/android/system/core/libsysutils/include/sysutils/SocketListener.h
class SocketListener {bool                    mListen;const char              *mSocketName;int                     mSock;SocketClientCollection  *mClients;pthread_mutex_t         mClientsLock;int                     mCtrlPipe[2];pthread_t               mThread;bool                    mUseCmdNum;public:SocketListener(const char *socketName, bool listen);SocketListener(const char *socketName, bool listen, bool useCmdNum);SocketListener(int socketFd, bool listen);virtual ~SocketListener();int startListener();int startListener(int backlog);int stopListener();void sendBroadcast(int code, const char *msg, bool addErrno);void runOnEachSocket(SocketClientCommand *command);bool release(SocketClient *c) { return release(c, true); }protected:virtual bool onDataAvailable(SocketClient *c) = 0;private:bool release(SocketClient *c, bool wakeup);static void *threadStart(void *obj);void runListener();void init(const char *socketName, int socketFd, bool listen, bool useCmdNum);
};

SocketListener类的内容挺多的,使用时在一一解释吧。

所以我们可以看到当new NetlinkHandler时传入的socket,第一步是调用NetlinkHandler的构造函数,在函数中将socket传给了NetlinkListener类;进一步,NetlinkListener类的构造函数又将socket传给了SocketListener类。并调用SocketListener类的init函数。

6.5 SocketListener::init()
SocketListener::init函数是在SocketListener类的构造函数调用的,所以new类时就对调用到。构造函数内容如下:

SocketListener::SocketListener(const char *socketName, bool listen) {init(socketName, -1, listen, false);
}
SocketListener::SocketListener(int socketFd, bool listen) {init(NULL, socketFd, listen, false);
}

我们可以看到SocketListener构造函数调用init时传入了NULL、socketFd、 listen和false。
socketFd:是从NetLinkManager.start()函数申请的socket描述符经过NetlinkHandler类->NetlinkListener 类->SocketListener 类,一直传入的。
listen:由NetlinkListener 类构造函数传入,也是固定为false。
我们接着看init函数的内容,如下:

路径:LINUX/android/system/core/libsysutils/src/SocketListener.cpp
void SocketListener::init(const char *socketName, int socketFd, bool listen, bool useCmdNum) {mListen = listen;mSocketName = socketName;mSock = socketFd;mUseCmdNum = useCmdNum;pthread_mutex_init(&mClientsLock, NULL);mClients = new SocketClientCollection();
}

从SocketListener构造函数调用可知:
mListen : mListen =listen=false;
mSocketName :mSocketName = NULL
mSock :mSock = socketFd = NetlinkManager.mSock;
mUseCmdNum : mUseCmdNum = useCmdNum = false;
pthread_mutex_init(&mClientsLock, NULL):初始化锁;
SocketListener::init又new了一个SocketClientCollection类。 接着继续看该类定义。

6.6 SocketClientCollection定义
看下SocketClientCollection的定义,如下:

路径:LINUX/android/system/core/libsysutils/include/SocketClientCollection.h
class SocketClient {int             mSocket;bool            mSocketOwned;pthread_mutex_t mWriteMutex;....}
typedef android::sysutils::List<SocketClient *> SocketClientCollection;

可以看到是以SocketClient 类为成员的集合List。list类的定义在“LINUX/android/system/core/libsysutils/include/sysutils/List.h”中;
这个后续又多个socket客户端时使用存放socket的。

前面我们分析了NetlinkHandler类申请对象涉及到的层层父类;
这里初步整理一个NetLinkManager类的关系。如下图:
在这里插入图片描述

6.7 NetlinkHandler.start()
在第三章的5.3节中我们知道NetlinkManager::start()在为NetlinkHandler new了一个对象之后调用其start()函数,我们看下该函数的内容,如下:

路径:LINUX/android/system/vold/NetlinkHandler.cpp
int NetlinkHandler::start() {//NetlinkHandler的start函数调用的startListener函数正是 NetlinkListener 的父类 SocketListener 类中的实现return this->startListener();   
}

我们可以看到NetlinkHandler的start函数调用的是NetlinkHandler的父类NetlinkListener 的startListener函数,这也正是 NetlinkListener 的父类 SocketListener 类中的实现方法。
NetlinkHandler->NetlinkListener ->SocketListener::startListener()

6.8 SocketListener.startListener()
接着看看该函数的内容。

路径:LINUX/android/system/core/libsysutils/src/SocketListener.cpp
int SocketListener::startListener() {return startListener(4);//进一步调用带参函数,带入参数4
}int SocketListener::startListener(int backlog) {//此函数的 mSock 是在 NetlinkManager::start 中  mHandler = new NetlinkHandler(mSock);  传入的if (!mSocketName && mSock == -1) {              SLOGE("Failed to start unbound listener");errno = EINVAL;return -1;} else if (mSocketName) {if ((mSock = android_get_control_socket(mSocketName)) < 0) {SLOGE("Obtaining file descriptor socket '%s' failed: %s", mSocketName, strerror(errno));return -1;}SLOGV("got mSock = %d for %s", mSock, mSocketName);fcntl(mSock, F_SETFD, FD_CLOEXEC);}if (mListen && listen(mSock, backlog) < 0) {SLOGE("Unable to listen on socket (%s)", strerror(errno));return -1;} else if (!mListen)mClients->push_back(new SocketClient(mSock, false, mUseCmdNum));if (pipe(mCtrlPipe)) {SLOGE("pipe failed (%s)", strerror(errno));return -1;}if (pthread_create(&mThread, NULL, SocketListener::threadStart, this)) {SLOGE("pthread_create (%s)", strerror(errno));return -1;}return 0;
}

第3行:进一步调用带参函数,并带入参数4;
第8行:在6.5节时解释了各个变量的值如下:
mListen =false;
mSocketName = null
mSock = NetlinkManager::mSock;
mUseCmdNum = false;
所以第6行的判断不进入。
第12行:因为mSocketName = null 所以也不进入;因此if里面的android_get_control_socket()函数和fcntl()函数先不解释,后续有用到在解释。
第22行:listen()将sockfd引用的套接字标记为被动套接字,也就是说,标记为将使用accept()函数接受传入连接请求的套接符。

sockfd参数:是一个文件描述符,它引用SOCK_STREAM或SOCK_SEQPACKET类型的套接字。
backlog参数:定义了sockfd的挂起连接队列可能增长的最大长度。如果连接请求在队列已满时到达,则客户端可能会收到一个带有ECONREFUSED指示的错误,或者,如果底层协议支持重传,则可以忽略该请求,以便稍后重新尝试连接成功。
返回值:成功后,返回零。出错时,返回-1,并适当设置errno。
这里sockfd = NetlinkManager::mSock,backlog=4。
正常情况下listen()返回大于0的值,不会进入该if判断。

第25行:由前面分析知mListen =false,所以这个判断会进入。
第26行:以mSock为参数,new了一个SocketClient,并放入到mClients,这个mClients是6.6节解释的一个list。后面分析SocketClient类。

路径:LINUX/android/system/core/libsysutils/include/sysutils/List.h
void push_back(const T& val) { insert(end(), val); }

由 push_back可知,是将new的SocketClient到mClients的list的最后。

第28行:mCtrlPipe是类里的定义的“int mCtrlPipe[2]”,pipe()创建一个管道,一个可用于进程间通信的单向数据通道。数组pipefd用于返回引用管道末端的两个文件描述符。pipefd[0]表示管道的读取端。pipefd[1]表示管道的写入端。写入管道写入端的数据由内核缓冲,直到从管道读取端读取。

第33行:pthread_create创建线程,用来创建socket的监听线程。
函数原型:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
描述:pthread_create()函数在调用进程中启动一个新线程。新线程通过调用start_routine()开始执行;arg作为start_routine()的唯一参数传递。
由传入的参数可知 调用的新线程为SocketListener::threadStart();传入的参数是this,则是当前这个SocketListener类对象的引用。

6.9 SocketListener类定义
在6.8节我们分析到,new了个SocketClient。来看看这个SocketClient有啥东西。

路径:LINUX/android/system/core/libsysutils/include/sysutils/SocketClient.h
class SocketClient {int             mSocket;bool            mSocketOwned;pthread_mutex_t mWriteMutex;pid_t mPid; //对等进程IDuid_t mUid; //对等用户IDgid_t mGid; //对等组IDpthread_mutex_t mRefCountMutex; //引用计数(从1开始)int mRefCount;int mCmdNum;bool mUseCmdNum;public:SocketClient(int sock, bool owned);SocketClient(int sock, bool owned, bool useCmdNum);virtual ~SocketClient();int getSocket() { return mSocket; }pid_t getPid() const { return mPid; }uid_t getUid() const { return mUid; }gid_t getGid() const { return mGid; }void setCmdNum(int cmdNum) {android_atomic_release_store(cmdNum, &mCmdNum);}int getCmdNum() { return mCmdNum; }//发送以空结尾的C字符串:int sendMsg(int code, const char *msg, bool addErrno);int sendMsg(int code, const char *msg, bool addErrno, bool useCmdNum);int sendMsg(const char *msg);//提供向客户端发送响应代码的机制。发送代码和空字符。int sendCode(int code);//提供向客户端发送二进制数据的机制。发送代码和一个空字符,后跟4字节的大端长度和数据。int sendBinaryMsg(int code, const void *data, int len);//发送二进制数据:int sendData(const void *data, int len);//iovec内容未通过调用保存int sendDatav(struct iovec *iov, int iovcnt);//可选引用计数。引用计数从1开始。如果它减到0,它会删除自身。//SocketListener创建一个SocketClient(在refcount 1),并在客户端完成后调用decRef()。void incRef();//在0处返回true(但注意:SocketClient已删除)bool decRef(); //为“my arg”传输返回带“\\”和“\”转义的引号的新字符串static char *quoteArg(const char *arg);private:void init(int socket, bool owned, bool useCmdNum);//正在发送二进制数据。调用者应该确保这一点不受多个线程同时进入的影响。//如果成功,则返回0;如果存在0字节写入或发生任何其他错误,则返回-1(使用errno获取错误)int sendDataLockedv(struct iovec *iov, int iovcnt);
};typedef android::sysutils::List<SocketClient *> SocketClientCollection;

我们可以看到该类定义了很对发送data的操作函数 以及一些变量。
该类的构造函数如下:

路径:
LINUX/android/system/core/libsysutils/src/SocketClient.cpp
SocketClient::SocketClient(int socket, bool owned) {init(socket, owned, false);
}SocketClient::SocketClient(int socket, bool owned, bool useCmdNum) {init(socket, owned, useCmdNum);
}

根据SocketListener.startListener()中new时带入的参数可知,这里调用的构造函数时带3个参数的。
socket=mSock
owned= false
useCmdNum=mUseCmdNum=false

构造函数调用的SocketClient::init(),函数内容如下:

路径:LINUX/android/system/core/libsysutils/src/SocketClient.cpp
void SocketClient::init(int socket, bool owned, bool useCmdNum) {mSocket = socket;mSocketOwned = owned;mUseCmdNum = useCmdNum;pthread_mutex_init(&mWriteMutex, NULL);pthread_mutex_init(&mRefCountMutex, NULL);mPid = -1;mUid = -1;mGid = -1;mRefCount = 1;mCmdNum = 0;struct ucred creds;socklen_t szCreds = sizeof(creds);memset(&creds, 0, szCreds);int err = getsockopt(socket, SOL_SOCKET, SO_PEERCRED, &creds, &szCreds);if (err == 0) {mPid = creds.pid;mUid = creds.uid;mGid = creds.gid;}
}

第3行:mSocket =socket = NetlinkManager::mSock;
第4行:owned= false
第5行:useCmdNum=mUseCmdNum=false
第6行:初始化mWriteMutex,线程写锁;
第7行:初始化mRefCountMutex,线程计数锁;
第8~12行,初始化相关变量值。
第14行:定义一个 struct ucred 结构体变量 creds;
结构体如下:

路径: usr/include/x86_64-linux-gnu/bits/socket.h
struct ucred
{pid_t pid;			/* PID of sending process.  */uid_t uid;			/* UID of sending process.  */gid_t gid;			/* GID of sending process.  */
};

第15行:定义一个socklen_t类型的 szCreds,值为sizeof(creds);socklen_t的类型是unsigned int;pid_t的类型是int;uid_t的类型是unsigned int;unsigned int的类型是int;所以szCreds的值为12;
第16行:creds清0;
第18行:获取socket的描述符引用的套接字的选项;

函数原型:
int getsockopt(int sockfd, int level, int optname,void *optval, socklen_t *optlen);
参数optval和optlen用于访问setsockopt()的选项值。对于getsockopt(),它们标识一个缓冲区,请求的选项的值将在该缓冲区中返回。optlen是一个值结果参数,最初包含optval指向的缓冲区的大小,并在返回时进行修改以指示返回值的实际大小。如果未提供或返回任何选项值,optval可能为NULL。
Optname和任何指定的选项都会无解释地传递给相应的协议模块进行解释。
参数level:在6.1节有解释,在这个环境下固定使用SOL_SOCKET;
参数optval:也是在6.1节有解释,SO_PEERCRED仅适用于由sockpair创建的AF_UNIX流套接字或AF_UNIX流/数据报套接字。
返回值:成功后,标准选项返回0。出错时,返回-1,并适当设置errno。

第19~22行:由前面函数的分析可知,getsockopt()获取到对应的pid、uid、gid,并提取赋值给到SocketClient类的成员变量中。

6.10 SocketListener::threadStart()

前面讲到NetlinkManager::start() 调用了NetlinkHandler::start(),进一步调用了SocketListener::startListener();在startListener中又调用了pthread_create()函数创建线程SocketListener::threadStart();
前面对NetlinkManager的前期准备工作基本都做完了,剩下的就是具体的监听Kernel的uevent的事了。
我们接着看。
SocketListener::threadStart()函数内容如下:

路径:LINUX/android/system/core/libsysutils/src/SocketListener.cpp
void *SocketListener::threadStart(void *obj) {SocketListener *me = reinterpret_cast<SocketListener *>(obj);me->runListener();pthread_exit(NULL);return NULL;
}

第3行:SocketListener::startListener()调用创建这个线程是传入的参数this,表示的是SocketListener这个类对象。
第4行:调用的SocketListener::runListener()方法;
第5行:线程退出。

6.11 SocketListener::runListener()
接着继续看runListener函数的内容,这个函数有点长,不过没关系,我们来一一分析。这个函数的主要内容是一直循环监测所有的SocketClient;

路径:LINUX/android/system/core/libsysutils/src/SocketListener.cpp
void SocketListener::runListener() {SocketClientCollection pendingList;while(1) {SocketClientCollection::iterator it;fd_set read_fds;int rc = 0;int max = -1;FD_ZERO(&read_fds);if (mListen) {max = mSock;FD_SET(mSock, &read_fds);}FD_SET(mCtrlPipe[0], &read_fds);if (mCtrlPipe[0] > max)max = mCtrlPipe[0];pthread_mutex_lock(&mClientsLock);for (it = mClients->begin(); it != mClients->end(); ++it) {//注意:在保持mClientsLock的情况下调用其他对象(安全)int fd = (*it)->getSocket();FD_SET(fd, &read_fds);if (fd > max) {max = fd;}}pthread_mutex_unlock(&mClientsLock);SLOGV("mListen=%d, max=%d, mSocketName=%s", mListen, max, mSocketName);if ((rc = select(max + 1, &read_fds, NULL, NULL, NULL)) < 0) {if (errno == EINTR)continue;SLOGE("select failed (%s) mListen=%d, max=%d", strerror(errno), mListen, max);sleep(1);continue;} else if (!rc)continue;if (FD_ISSET(mCtrlPipe[0], &read_fds)) {char c = CtrlPipe_Shutdown;TEMP_FAILURE_RETRY(read(mCtrlPipe[0], &c, 1));if (c == CtrlPipe_Shutdown) {break;}continue;}if (mListen && FD_ISSET(mSock, &read_fds)) {int c = TEMP_FAILURE_RETRY(accept4(mSock, nullptr, nullptr, SOCK_CLOEXEC));if (c < 0) {SLOGE("accept failed (%s)", strerror(errno));sleep(1);continue;}pthread_mutex_lock(&mClientsLock);mClients->push_back(new SocketClient(c, true, mUseCmdNum));pthread_mutex_unlock(&mClientsLock);}/*首先将所有活动客户端添加到挂起列表*/pendingList.clear();pthread_mutex_lock(&mClientsLock);for (it = mClients->begin(); it != mClients->end(); ++it) {SocketClient* c = *it;//注意:在保持mClientsLock的情况下调用其他对象(安全)int fd = c->getSocket();if (FD_ISSET(fd, &read_fds)) {pendingList.push_back(c);c->incRef();}}pthread_mutex_unlock(&mClientsLock);/*处理挂起的列表,因为它属于线程,所以不需要锁定它*/while (!pendingList.empty()) {/*从列表中弹出第一项*/it = pendingList.begin();SocketClient* c = *it;pendingList.erase(it);/*处理它,如果返回false,则从列表中删除*/if (!onDataAvailable(c)) {      //将收到信息的SocketClient对象指针传递到了onDataAvailable函数中release(c, false);}c->decRef();}}
}

第3行:由6.6节分析知,SocketClientCollection是一个以SocketClient为成员节点的list。
第6行:以LIST类里的_ListIterator子类定义变量it;
所以it是一个_ListIterator类,类定义在LINUX/android/system/core/libsysutils/include/sysutils/List.h中。
第7行:定义fd_set类型的变量 read_fds。fd_setfd_set用于select和pselect。定义在“usr/include/x86_64-linux-gnu/sys/select.h”; fd_set其实这是一个数组的宏定义,实际上是一long类型的数组,每一个数组元素都能与一打开的文件句柄(socket、文件、管道、设备等)建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪个句柄可读。
第10行:将read_fds清零使集合中不含任何fd(描述符/句柄);
第12行:在6.5节介绍SocketListener的构造函数时解释了mListen=false;所以第10、11行不会执行到;
第17行:在6.8节定义中知道mCtrlPipe[0]表示管道的读取端、mCtrlPipe[1]表示管道的写入端。这里是将mCtrlPipe[0]读取端管道加入到read_fds集合。
第18~19行:有前面几句分析可知mCtrlPipe[0] > max。所以会执行第15行。
第21行:使用mClientsLock锁,在6.5分析SocketListener构造函数时执行了mClientsLock的初始化;
第22~29行:遍历mClients这个List,获取对应节点后将其fd描述符 加入到read_fds集合;并将最大的fd描述符赋值给max变量。
//注意:在保持mClientsLock的情况下调用其他对象(安全)
第30行:释放mClientsLock锁。
第32行:首先先来看看select()这个函数的定义与解释;

函数原型:int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
参数解释:
nfds:是⼀个整数值,是指集合中所有⽂件描述符的范围,即所有⽂件描述符的最⼤值加1,不能错。所以我们看到前面为什么要获取max这个值了。
readfds:是指向 fd_set 结构的指针,这个集合中包括⽂件描述符,是要监视这些⽂件描述符的读变化的,即关⼼是否可以从这些⽂件中读取数据。所以看到前面把mCtrlPipe[0]读取端管道和mClients的描述符都放进这个read_fds中。如果这个集合中有⼀个⽂件可读,select就会返回⼀个⼤于0的值,表⽰有⽂件可读(前提没有超时); 如果没有可读的⽂件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发⽣错误返回负值。 可以传⼊NULL值,表⽰不关⼼任何⽂件的读变化。
writefds:是指向 fd_set 结构的指针,这个集合中包括⽂件描述符,是要监视这些⽂件描述符的写变化的,即关⼼是否可以向这些⽂件中写⼊数据。 如果这个集合中有⼀个⽂件可写,select就会返回⼀个⼤于0的值,表⽰有⽂件可写; 如果没有可写的⽂件,则根据timeout再判断是否超时,若超出timeout的时间,select返回0,若发⽣错误返回负值。可以传⼊NULL值,表⽰不关⼼任何⽂件的写变化。
exceptfds:⽤来监视⽂件错误异常。这个通常是:OOB同步标记未处理。timeout:是select的超时时间,这个参数⾄关重要,它可以使select处于三种状态。
状态1:若将NULL以形参传⼊,即不传⼊时间结构,就是将select置于阻塞状态,⼀定等到监视⽂件描述符集合中某个⽂件描述符发 ⽣变化为⽌;
状态2:若将时间值设为00毫秒,就变成⼀个纯粹的⾮阻塞函数,不管⽂件描述符是否有变化,都⽴刻返回继续执⾏,⽂件⽆变化 返回0,有变化返回⼀个正值;
状态3:timeout的值⼤于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在 超时后不管怎样⼀定返回,若超出timeout的时间,select返回0,若发⽣错误返回负值。
struct timeval结构体定义如下:
struct timeval{
long tv_sec;   /*秒 */
long tv_usec;  /*微秒 */
}
所以我们可以知道第27行是以阻塞的形式监控read_fds存放的所有mCtrlPipe[0]读取端管道和mClients的描述符是否发生变化的。

第33~37行:如果监控的描述符发生错误返回负值,则进入这部分执行,主要是输出Log。
第38行:select()超时返回0,但由于select()的参数是NULL,表示阻塞,所以不会有超时的情况;第39行也就执行不到。
第41行:检测mCtrlPipe[0]读取端管道是否在read_fds集合中,不在则返回0。前面我们分析知道,mCtrlPipe[0]读取端管道是加入到这个集合中的。所以会执行第42~47行。
第42行:定义一个char类的变量c,并赋值CtrlPipe_Shutdown,CtrlPipe_Shutdown为0。
第43行:先分析read()函数,如下

函数原型:ssize_t read(int fd, void *buf, size_t count)
参数解析:
fd:文件描述符,用来指向要操作的文件的文件结构体buf:一块内存空间count:希望读取的字节数
由此可知,这里的read是读取mCtrlPipe[0]管道中的1字节,并放到c中;
TEMP_FAILURE_RETRY()又是什么呢?
TEMP_FAILURE_RETRY()是个宏定义,内容如下:
路径:usr/include/unistd.h
# define TEMP_FAILURE_RETRY(expression) \(__extension__							      \({ long int __result;						      \do __result = (long int) (expression);				      \while (__result == -1L && errno == EINTR);			      \__result; }))
该宏的作用是如果返回指定错误,那么进行重试;可以看到如果表达式返回错误是EINTR,那么就进行重试,其中 :
#define    EINTR    4    /* Interrupted system call */
错误4代表什么情景可以发生呢?
如果一个进程在一个慢系统调用中阻塞时,当进程捕获到某个信号且相应从信号 处理函数返回时,这个系统调用被中断,然后返回错误,并且设置errno为EINTR。
什么是为慢系统调用呢?(1) 读写一个设备时,当缓存满时,需要阻塞等待资源,或者其他原因需要阻塞.(2) 访问一些互斥资源时,当前条件不满足时(3) waitpid, accept(阻塞),ioctl等等,只要引起阻塞的系统调用,那么就可以称为慢系统调用.
而上面的这个表达式就是为了解决EINTR返回而设计的,但也有其局限性,有些调用不能简单的重新调用来避免这个问题.比如connect函数返回一个EINTR错误的时候,不能再次调用它,否则会马上返回一个错误,错对这 种的解决方法是用select(connect_fd)来做。
防止EINTR还有一种方法,那就是直接屏蔽此信号,不让系统产生信号中断。

第44行:如果上一句read()函数没有读到数据,那么c依旧=CtrlPipe_Shutdown;
第45行: 这里就直接break。表示mCtrlPipe[0]读管道关闭,就读不了数据了。监听kernel 的线程也就结束了。

第49行:由6.5节的init函数可知,mListen为false,所以这个判断里的内容不会执行。不执行就先不分析了。
第62行:清理用于挂起的LIST,这个在函数开头就定义了这个列表。
第63行: 加锁;
第64-72行:将mClients这里LIST中一个一个取出Socket,并判断该Socket是否在read_fds这个集合中,如果在,则放到pendingList这个LIST中,然后该SocketClient的引用计数+1。
第73行:释放锁;
第76-86行:取出pendingList这个LIST中的一个it,并赋给 c,然后删除it,接着调用onDataAvailable©对c 这个SocketClient进行处理,处理完的SocketClient *c 需要release调,然后SocketClient *c的引用计数-1。
这样则对所有的 SocketClient 都会处理到。具体的处理函数数onDataAvailable(),接着后面看看这个函数。

6.12 NetlinkListener::onDataAvailable()
在6.11节分析中,知道onDataAvailable()函数是在SocketListener 中调用的,也从前面的分析知道 NetlinkListener类继承了SocketListener 类,所以SocketListener 中的虚函数onDataAvailable在NetlinkListener类中实现了方法;所以6.11节中提到的调用的onDataAvailable()函数,实际是NetlinkListener类中的onDataAvailable()函数;因此我们要分析的就是NetlinkListener类中的onDataAvailable()函数。
看看该函数的内容如下:

路径:LINUX/android/system/core/libsysutils/src/NetlinkListener.cpp
bool NetlinkListener::onDataAvailable(SocketClient *cli)
{int socket = cli->getSocket();ssize_t count;uid_t uid = -1;bool require_group = true;if (mFormat == NETLINK_FORMAT_BINARY_UNICAST) {require_group = false;}//根据socket通过uevent_kernel_recv获取底层Uevent事件,并将其存储到mBuffer中//socket创建的时候就确认了有哪些uevent事件需要关注count = TEMP_FAILURE_RETRY(uevent_kernel_recv(socket,mBuffer, sizeof(mBuffer), require_group, &uid));if (count < 0) {SLOGE("recvmsg failed (%s)", strerror(errno));return false;}NetlinkEvent *evt = new NetlinkEvent();if (evt->decode(mBuffer, count, mFormat)) {onEvent(evt);   //将消息传递到onEvent函数} else if (mFormat != NETLINK_FORMAT_BINARY) {//如果parseBinaryNetlinkMessage返回false,不要抱怨。这可能意味着缓冲区中没有我们感兴趣的消息。SLOGE("Error decoding NetlinkEvent");}delete evt;return true;
}

第4行:获取SocketClient的Socket套节字;
第5-8行:定义一些变量;
第9行:判断格式是否是 网络链接格式二进制单播 ;是则require_group=false,非组播的意思;
第14行: TEMP_FAILURE_RETRY 是一个宏,用于忽略系统中断造成的错误。常用于系统调用。具体内容如下:

/*对EXPRESSION求值,并重复,只要它返回-1并带有“errno”设置为EINTR*/
# define TEMP_FAILURE_RETRY(expression) \
(__extension__ \
({ long int __result; \
do __result = (long int) (expression); \
while (__result == -1L && errno == EINTR); \
__result; }))

uevent_kernel_recv()函数主要是用来接收kernel的uevent,属于系统调用的一个接口。看看具体内容。

路径:LINUX/android/system/core/libcutils/uevent.c
ssize_t uevent_kernel_recv(int socket, void *buffer, size_t length, bool require_group, uid_t *uid)
{struct iovec iov = { buffer, length };struct sockaddr_nl addr;char control[CMSG_SPACE(sizeof(struct ucred))];struct msghdr hdr = {&addr,sizeof(addr),&iov,1,control,sizeof(control),0,};*uid = -1;ssize_t n = recvmsg(socket, &hdr, 0);if (n <= 0) {return n;}struct cmsghdr *cmsg = CMSG_FIRSTHDR(&hdr);if (cmsg == NULL || cmsg->cmsg_type != SCM_CREDENTIALS) {/*忽略没有发件人凭据的netlink消息*/goto out;}struct ucred *cred = (struct ucred *)CMSG_DATA(cmsg);*uid = cred->uid;if (cred->uid != 0) {/*忽略来自非root用户的netlink消息*/goto out;}if (addr.nl_pid != 0) {/* ignore non-kernel *//*忽略非内核*/goto out;}if (require_group && addr.nl_groups == 0) {/*请求时忽略单播消息*/goto out;}return n;out:/*清除残留的潜在恶意数据*/bzero(buffer, length);errno = EIO;return -1;
}

从该函数看主要是调用 recvmsg()函数 读取对应socket数据。放到了struct msghdr hdr 这个结构体中,主要的数据是放在 struct iovec iov 中,而 struct iovec iov 中主要存放数据的则是 uevent_kernel_recv()函数的参数void *buffer。所以在 onDataAvailable()函数中调用uevent_kernel_recv()时传入的mBuffer。
针对recvmsg()函数 具体的通信,这里不再深入分析,后续有时间在详细分析 这一部分内容。

这里我们值要知道 是去读取kernel的数据即可。

第16行:在第14行读取错误返回负数进入判断,输出错误Log,并返回false。
第21行:new一个 NetlinkEvent类;
NetlinkEvent类的内容如下:

路径:LINUX/android/system/core/libsysutils/include/sysutils/NetlinkEvent.h
class NetlinkEvent {
public:enum class Action {kUnknown = 0,kAdd = 1,kRemove = 2,kChange = 3,kLinkUp = 4,kLinkDown = 5,kAddressUpdated = 6,kAddressRemoved = 7,kRdnss = 8,kRouteUpdated = 9,kRouteRemoved = 10,};private:int  mSeq;char *mPath;Action mAction;char *mSubsystem;char *mParams[NL_PARAMS_MAX];public:NetlinkEvent();virtual ~NetlinkEvent();bool decode(char *buffer, int size, int format = NetlinkListener::NETLINK_FORMAT_ASCII);const char *findParam(const char *paramName);const char *getSubsystem() { return mSubsystem; }Action getAction() { return mAction; }void dump();protected:bool parseBinaryNetlinkMessage(char *buffer, int size);bool parseAsciiNetlinkMessage(char *buffer, int size);bool parseIfInfoMessage(const struct nlmsghdr *nh);bool parseIfAddrMessage(const struct nlmsghdr *nh);bool parseUlogPacketMessage(const struct nlmsghdr *nh);bool parseNfPacketMessage(struct nlmsghdr *nh);bool parseRtMessage(const struct nlmsghdr *nh);bool parseNdUserOptMessage(const struct nlmsghdr *nh);
};

从该类的定义大致可看出, NetlinkEvent类 主要是用来解析 前面uevent_kernel_recv()接收到的uevent 的。具体处理接收的数据相关的函数,后面用到时再解释。
第22行:调用NetlinkEvent类的decode()方法,对接收数据进行解析;解析下一节单独解释;
第23行:解析完后调用onEvent()进行对解析后的uevent进行处理。
第24-27行:判断编码格式数据log;
第29-30行:最后释放evt后返回。

6.13 NetlinkEvent::decode()
在6.12节中newl 一个NetlinkEvent类;在前面已列出来。看看new后执行的构造函数,函数如下:

路径:LINUX/android/system/core/libsysutils/src/NetlinkEvent.cpp
NetlinkEvent::NetlinkEvent() {mAction = Action::kUnknown;memset(mParams, 0, sizeof(mParams));mPath = NULL;mSubsystem = NULL;
}

第3行:mAction 是用来标记将uevent的Action 的,初始设为kUnknown;
第4行:数组mParams清0;
第5-6行:清空指针。

看完NetlinkEvent类的初始化后,接着具体看一下decode()方法;

路径:LINUX/android/system/core/libsysutils/src/NetlinkEvent.cpp
bool NetlinkEvent::decode(char *buffer, int size, int format) {if (format == NetlinkListener::NETLINK_FORMAT_BINARY|| format == NetlinkListener::NETLINK_FORMAT_BINARY_UNICAST) {return parseBinaryNetlinkMessage(buffer, size);} else {return parseAsciiNetlinkMessage(buffer, size);}
}

可已看到,根据不同的编码格式,解析也分两种;在6.3节,我们可知编码格式是 mFormat = NETLINK_FORMAT_ASCII;所以我们这里主要分析parseAsciiNetlinkMessage()这个解析函数。

这里对源码填加了Log进行测试验证,内容如下:

bool NetlinkEvent::decode(char *buffer, int size, int format) {if (format == NetlinkListener::NETLINK_FORMAT_BINARY|| format == NetlinkListener::NETLINK_FORMAT_BINARY_UNICAST) {SLOGE("[watr] enter parseBinaryNetlinkMessage \n");return parseBinaryNetlinkMessage(buffer, size);} else {SLOGE("[watr] enter parseAsciiNetlinkMessage \n");return parseAsciiNetlinkMessage(buffer, size);}
}

输出Log如下:
在这里插入图片描述
有此可确认采用的是ascii 编码,跟前面分析的编码类型一致。所以调用的解析函数也就是parseAsciiNetlinkMessage()了。

接着看看parseAsciiNetlinkMessage()函数的内容。

路径:LINUX/android/system/core/libsysutils/src/NetlinkEvent.cpp
/* 分析NETLINK_KOBJECT_UEVENT网络链接套接字中的ASCII格式消息。*/
bool NetlinkEvent::parseAsciiNetlinkMessage(char *buffer, int size) {const char *s = buffer;const char *end;int param_idx = 0;int first = 1;if (size == 0)return false;/*确保缓冲区以零结尾,下面的代码取决于此*/buffer[size-1] = '\0';end = s + size;while (s < end) {if (first) {const char *p;/*缓冲区以0结尾,无需检查p<end*/for (p = s; *p != '@'; p++) {if (!*p) { /*没有“@”,不应该发生*/return false;}}mPath = strdup(p+1);//拷贝字符串 p+1 到mPath中,会申请内存,字符串跳过@first = 0; //取得一个字串后跑else部分} else {const char* a;if ((a = HAS_CONST_PREFIX(s, end, "ACTION=")) != NULL) { //检查以指定的字串开头if (!strcmp(a, "add"))      //比较字串是不是 addmAction = Action::kAdd; //设置动作 kAddelse if (!strcmp(a, "remove"))//比较字串是不是 removemAction = Action::kRemove;//设置动作 kRemoveelse if (!strcmp(a, "change"))//比较字串是不是 changemAction = Action::kChange;//设置动作 kChange} else if ((a = HAS_CONST_PREFIX(s, end, "SEQNUM=")) != NULL) {mSeq = atoi(a);} else if ((a = HAS_CONST_PREFIX(s, end, "SUBSYSTEM=")) != NULL) {mSubsystem = strdup(a);} else if (param_idx < NL_PARAMS_MAX) {mParams[param_idx++] = strdup(s);}}s += strlen(s) + 1;}return true;
}

第4行:定义一个const char *s 用来指向kernel上报的event buffer;
第6行:定义一个param_idx用以计数;
第9行:判断上报的数据长度是否=0,是则立即返回false;
第13行:给接收的数据字符串添加结束符;
第15行:end是字符串结束地址,由起始地址s+数据长度size;
第17~26行:判断buffer中是否存在字符@,不存在则返回false;存在,则拷贝字符串 p+1 到mPath中,会申请内存,字符串跳过@;并且将first清0;之后第16行的while循环就会进入第27行的else部分了;
第27行:检查以指定的字串"ACTION="开头,存在则a不为NULL,进入第30~35行的判断;
第30行:比较第29行返回的地址之后是否存在“add”,是则设置mAction = Action::kAdd;
第32行:比较第29行返回的地址之后是否存在“remove”,是则设置mAction = Action::kRemove;
第34行:比较第29行返回的地址之后是否存在“change”,是则设置mAction = Action::kChange;
第36行:检查以指定的字串"SEQNUM="开头,存在则a不为NULL
第37行: 字符转数字,SEQNUM数字。
第38行:检查以指定的字串"SUBSYSTEM="开头,存在则a不为NULL
第39行: 截取a之后的字符串给到mSubsystem。
第40~41:判断获取的属性个数数否超阀值,并将获取的字符串放到参数列表里。
第44行:跳转到下一个属性地址。
第46行,解析完成后返回true。
由6.13节分析可知,当正常接收到uevent的属性数据后,

NetlinkListener::onDataAvailable()中调用的evt->decode(mBuffer, count, mFormat)会返回true,因此调用onEvent(evt); 将消息传递到onEvent函数。
此时的onEvent函数是“LINUX/android/system/vold/NetlinkHandler.cpp”文件中的void NetlinkHandler::onEvent(NetlinkEvent *evt)函数,而传入的参数正是NetlinkListener::onDataAvailable()中new的NetlinkEvent()对象的 evt。

6.14 NetlinkHandler::onEvent()
onEvent()函数是用来处理 NetlinkListener类处理uevent数据后的数据的。接下来看下该函数的内容。

路径:LINUX/android/system/vold/NetlinkHandler.cpp
// 获取到 kernel 事件 后调用这个函数进行处理
void NetlinkHandler::onEvent(NetlinkEvent *evt) {VolumeManager *vm = VolumeManager::Instance(); //获取一个VolumeManager 对象const char *subsys = evt->getSubsystem(); //获取子系统if (!subsys) {  //无子系统SLOGW("No subsystem found in netlink event");return;}if (!strcmp(subsys, "block")) { //判断是块设备子系统vm->handleBlockEvent(evt); //执行 VolumeManager 中的块设备event}
}

以U盘插入为例,U盘为存储设备,在kernel中是分类为块设备的。所以当U盘插入后,在onEvent()中判断为block ,所以会执行vm->handleBlockEvent(evt)。

至此vold获取kernel的uevent数据接收部分已分析完,后续就是VolumeManager对uevent的处理了。

通过VolumeManager::handleBlockEvent()函数运行到VolumeManager类中。

VolumeManager类将在下一篇讲解。


http://chatgpt.dhexx.cn/article/33S6s2IP.shtml

相关文章

<Android开发> Android vold - 第二篇 vold 的main()函数简介

本系列主要介绍 Android vold&#xff0c;分为以下篇章 &#xff1c;Android开发&#xff1e; Android vold - 第一篇 vold前言简介 &#xff1c;Android开发&#xff1e; Android vold - 第二篇 vold 的main()函数简介 &#xff1c;Android开发&#xff1e; Android vold - 第…

Android外部存储设备管理——vold挂载大容量存储设备

一、简介 Vold(volume Daemon)&#xff0c;即Volume守护进程&#xff0c;用来管理Android中存储类(USB-Storage&#xff0c;包含U盘和SD卡&#xff09;的热拔插事件&#xff0c;处于Kernel和Framework之间&#xff0c;是两个层级连接的桥梁。Vold在系统中以守护进程存在&#x…

Android Vold 架构简析

这篇文章中主要是分析一下&#xff0c;android系统里面的Vold——Vold是andorid系统的设备管理器&#xff0c;扮演着linux里面的udev的角色。它通过监听uevent的端口&#xff0c;取得 uevent事件&#xff0c;dispatch到 相应的Listener&#xff0c;执行相应的动作。 UEvent 在…

Android Vold架构

1. 总体架构 2. 流程概览 2.1 开启Vold 2.2 引导Uevent 2.3 处理事件 Vold - Volume Daemon存储类的守护进程&#xff0c;作为Android的一个本地服务&#xff0c;负责处理诸如SD、USB等存储类设备的插拔等事件。 1. 总体架构 Vold服务由volumeManager统一管控&#xff0c…

Vold 流程介绍

文章目录 前言框架MountService 流程Vold 流程 前言 印象中是参考 《深入理解 Android 卷 1 》 追的流程&#xff0c;差不多供参考吧 基于安卓 4.4 框架 MountService 流程 /*【初始化流程总结】&#xff1a; SystemServerinitAndLoop()// 创建 MountService 服务对象mountS…

ANDROID中的VOLD分析

现在可能很少有人会用mknod这个命令了&#xff0c;也很少有使用它的机会&#xff0c;但就在几年前&#xff0c;这还是一项linux工程师的必备技能&#xff0c;在制作文件系统前或加载新的驱动前&#xff0c;我们必须小心翼翼的创建设备节点。 不需要使用mknod并不是他消失了&am…

Android -- Vold机制简要分析

Android -- Vold机制简要分析 Vold是用于管理和控制Android外部存储介质的后台进程&#xff0c;这里说的管控&#xff0c;主要包括SD卡的插拔、挂载/卸载和格式化等&#xff1b;它是Android平台外部存储系统的管控枢纽。 Vold的整个控制模块主要由三个类模块构成&#xff1a;Ne…

Android Vold简介(一)

Vold(volume Daemon)&#xff0c;即Volume守护进程&#xff0c;用来管理Android中存储类的热拔插事件&#xff0c;处于Kernel和Framework之间&#xff0c;是两个层级连接的桥梁。先来看一下Vold在Android系统的整体架构。 该图主要包含了Framework和Vold进程的&#xff0c;Ke…

vold

一、Vold工作机制 Vold是Volume Daemon的缩写&#xff0c;它是Android平台中外部存储系统的管控中心&#xff0c;是管理和控制Android平台外部存储设备的后台进程。其功能主要包括&#xff1a;SD卡的插拔事件检测、SD卡挂载、卸载、格式化等。 如上图所示&#xff0c;Vold中的…

Vold工作流程分析学习

一 Vold工作机制分析 vold进程&#xff1a;管理和控制Android平台外部存储设备&#xff0c;包括SD插拨、挂载、卸载、格式化等&#xff1b; vold进程接收来自内核的外部设备消息。 Vold框架图如下&#xff1a; Vold接收来自内核的事件&#xff0c;通过netlink机制。 Netlink 是…

Android vold介绍

目录 1. 前言2. vold概述3. vold初始化|- -vm->start()|- -process_config|- -hardware::configureRpcThreadpool|- -vold::VoldNativeService::start|- -nm->start() 4. StorageManagerService|- -SM与vold建立关联|- - -startService(serviceClass) |- -StorageManager…

<Android开发> Android vold - 第一篇 vold前言简介

本系列主要介绍 Android vold&#xff0c;分为以下篇章 &#xff1c;Android开发&#xff1e; Android vold - 第一篇 vold前言简介 &#xff1c;Android开发&#xff1e; Android vold - 第二篇 vold 的main()函数简介 &#xff1c;Android开发&#xff1e; Android vold - 第…

Vold原理介绍

一、 Vold简介 Android中Vold是volume Daemon&#xff0c;即Volume守护进程&#xff0c;用来管理Android中存储类的热拔插事件。这里的热插拔涉及的场景如&#xff1a; 1. 手机usb以MTP或者传输照片方式插拔PC端后磁盘数据的挂卸载&#xff1b; 2. 设备开关机过程中存储设备各分…

C语言Switch....case用法

概述 C语言switch语句通常用于多个条件判断&#xff0c;根据不同情况执行不同的代码块。它的使用形式如下&#xff1a; switch&#xff08;表达式&#xff09; { case 常量表达式1&#xff1a; 语句序列1 break; case 常量表达式2&#xff1a; 语句序列2 break; ...... default…

MySQL 入门:Case 语句很好用

引言 MySQL CASE 是一个 MySQL 语句查询关键字&#xff0c;它定义了处理循环概念以执行条件集并使用 IF ELSE 返回匹配案例的方式。 MySQL 中的 CASE 是一种控制语句&#xff0c;它验证条件案例集&#xff0c;并在第一个案例满足 else 值时显示值并退出循环。 如果没有找到 T…

Shell 编程之 case 语句

一、case 语句 1、case 语句概述 (1)case 语句的作用 使用 case 语句改写 if 多分支可以使脚本结构更加清晰、层次分明。针对变量的不同取 值&#xff0c;执行不同的命令序列。 2、case 语句的结构: case 变量值 in 模式 1) 命令序列 1 ;; 模式 2) 命令序列 2 ;; *…

【shell】case实现简单的系统工具箱

case实现简单的系统工具箱 case实现简单的系统工具箱 #!/usr/bin/bash #system manage #by racon 2020-04-19menu() {cat <<-EOF########################################## h. help ## f. disk partition ## d. filesystem mount ## m. memory ## u. system lo…

SQL CASE语句的使用

SQL CASE语句的使用 CASE是一个控制流语句&#xff0c;其作用与IF-THEN-ELSE语句非常相似&#xff0c;可根据数据选择值。 CASE语句遍历条件并在满足第一个条件时返回值。 因此&#xff0c;一旦条件成立&#xff0c;它将短路&#xff0c;从而忽略后面的子句并返回结果。 正如我…

数据库 case 用法

【转载】:数据库中case when 的用法 CASE WHEN 及 SELECT CASE WHEN的用法Case具有两种格式。简单Case函数和Case搜索函数。 1.简单Case函数 CASE sex WHEN 1 THEN 男 WHEN 2 THEN 女 ELSE 其他 END 2.Case搜索函数 CASE WHEN sex 1 THEN 男 WHEN sex 2 THEN 女 ELSE 其他 …

SQL之CASE WHEN用法详解

简单CASE WHEN函数&#xff1a; CASE SCORE WHEN A THEN 优 ELSE 不及格 END CASE SCORE WHEN B THEN 良 ELSE 不及格 END CASE SCORE WHEN C THEN 中 ELSE 不及格 END 等同于&#xff0c;使用CASE WHEN条件表达式函数实现&#xff1a; CASE WHEN SCORE A THEN 优WHEN SCORE…