源码解读Binder机制二(SurfaceFlinger 如何向 ServiceManager 注册)

上文用简单的 AIDL 实例应用程序之间的进程间通讯,它是一种 c/s 架构。

client端:Proxy.transact() 来发送事务请求;

server端:Stub.onTransact() 会接收到相应事务。

这篇介绍 Server 如何通过 binder 向 ServiceManager 注册服务的?

ServiceManager

源码中用到 binder 最多的就是 ServiceManager。Binder 通信采用C/S架构,从组件视角来说,包含 Client、Server、ServiceManager 以及 binder 驱动,其中 ServiceManager 用于管理系统中的各种服务。

Server 端注册服务和 Client 端获取服务的过程都需要 ServiceManager,这篇先了解下 ServiceManager 的启动流程。

ServiceManager 是由 init 进程解析 init.rc 文件而创建的,在 \frameworks\native\cmds\servicemanager\servicemanager.rc 中定义了可执行程序 /system/bin/servicemanager,所对应的源文件是 service_manager.c,也在同一目录下。

service servicemanager /system/bin/servicemanager
    class core animation
    user system
    group system readproc
    //。。。

 init 进程解析完后会执行 service_manager 的入口函数 main。

int main(int argc, char** argv)
{
    
    if (argc > 1) {
        driver = argv[1];
    } else {
        driver = "/dev/binder";
    }
    //打开binder驱动,申请128k字节大小的内存空间
    bs = binder_open(driver, 128*1024);
    
    //成为上下文管理者
    if (binder_become_context_manager(bs)) {
        ALOGE("cannot become context manager (%s)\n", strerror(errno));
        return -1;
    }

    //进入无限循环,处理client端发来的请求
    binder_loop(bs, svcmgr_handler);

    return 0;
}

binder 对应的几个重要函数已在代码中标注。

binder_open

struct binder_state *binder_open(const char* driver, size_t mapsize)
{
//...

    //通过系统调用陷入内核,打开Binder设备驱动
    bs->fd = open(driver, O_RDWR | O_CLOEXEC);
    if (bs->fd < 0) {
//...
    }
    //通过系统调用,ioctl获取binder版本信息
    if ((ioctl(bs->fd, BINDER_VERSION, &vers) == -1) ||
//...
    }

    bs->mapsize = mapsize;
    //通过系统调用,mmap内存映射,mmap必须是page的整数倍
    bs->mapped = mmap(NULL, mapsize, PROT_READ, MAP_PRIVATE, bs->fd, 0);
    if (bs->mapped == MAP_FAILED) {
        fprintf(stderr,"binder: cannot map device (%s)\n",
                strerror(errno));// binder设备内存无法映射
        goto fail_map;
    }

}

先调用 open() 打开 "/dev/binder" 的 binder 设备,open 函数属于 Linux中系统 IO,用于“打开”文件,代码打开一个文件意味着获得了这个文件的访问句柄。O_RDWR 表示以可读可写方式打开,这样就获取了 binder 的句柄 fd。

之后调用 ioctl 函数,它是一个系统调用,作用于一个文件描述符。它接收一个确定要进行的命令的数字和(可选地)另一个参数, 常常是一个指针,可以实现几个用来调试用的 ioctl 命令,这些命令可以从驱动拷贝相关的数据结构到用户空间。实现了 ioctl,用户程序所作的只是通过命令码 (cmd) 告诉驱动程序它想做什么,至于怎么解释这些命令和怎么实现这些命令,这都是驱动程序要做的事情。后续很多地方都用到此函数。这里 ioctl() 检验当前 binder 版本与 Binder 驱动层的版本是否一致。

接着调用 mmap 映射内存,将一个文件或者其他对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对应关系;实现这样的映射关系后,进程就可以采用指针的方式读写操作这一块内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必调用 read,write 等系统调用函数,相反,内核空间堆这段区域的修改也直接反应到用户空间,从而可以实现不同进程间的文件共享。

bs 的结构: 

struct binder_state
{
    int fd;
    void *mapped;
    size_t mapsize;
};

这里就先了解这么多,具体驱动层的事情我也不太清楚,也不是 Android 软件开发人员关注的重点,想要深入了解可点击这里

binder_become_context_manager

int binder_become_context_manager(struct binder_state *bs)
{
    return ioctl(bs->fd, BINDER_SET_CONTEXT_MGR, 0);
}

这里将 binder 注册成为上下文的管理者,也就是整个系统只有这样一个管理者。

binder_loop

void binder_loop(struct binder_state *bs, binder_handler func)
{
    readbuf[0] = BC_ENTER_LOOPER;
    //将BC_ENTER_LOOPER命令发送给binder驱动,让Service Manager进入循环
    binder_write(bs, readbuf, sizeof(uint32_t));

    for (;;) {
      
        //进入循环,不断地binder读写过程
        res = ioctl(bs->fd, BINDER_WRITE_READ, &bwr);

        // 解析binder信息
        res = binder_parse(bs, 0, (uintptr_t) readbuf, bwr.read_consumed, func);
       
    }
}

binder_handler 指向 service_manager 的 svcmgr_handler 函数(之后交互会用到),通过 ioctl() 将 BC_ENTER_LOOPER 命令发送给 binder 驱动,之后进入 for 循环,执行 ioctl(),不断的从 binder 中读写数据、解析数据。

这样 ServiceManager 就启动了。

binder_parse 这里不细看了,svcmgr_handler 了解下函数的实现

int svcmgr_handler(struct binder_state *bs,
                   struct binder_transaction_data *txn,
                   struct binder_io *msg,
                   struct binder_io *reply)
{
    struct svcinfo *si;
    uint16_t *s;
    size_t len;
    uint32_t handle;
    uint32_t strict_policy;
    int allow_isolated;

    if (txn->target.ptr != BINDER_SERVICE_MANAGER)
        return -1;

    if (txn->code == PING_TRANSACTION)
        return 0;

    strict_policy = bio_get_uint32(msg);
    s = bio_get_string16(msg, &len);
   
    switch(txn->code) {
    case SVC_MGR_GET_SERVICE:
    case SVC_MGR_CHECK_SERVICE:
        //服务名
        s = bio_get_string16(msg, &len);
        //根据名称查找相应服务
        handle = do_find_service(s, len, txn->sender_euid, txn->sender_pid);
        if (!handle)
            break;
        bio_put_ref(reply, handle);
        return 0;
        
    case SVC_MGR_ADD_SERVICE:
        s = bio_get_string16(msg, &len);
        if (s == NULL) {
            return -1;
        }
        handle = bio_get_ref(msg);
        allow_isolated = bio_get_uint32(msg) ? 1 : 0;
        //注册指定服务
        if (do_add_service(bs, s, len, handle, txn->sender_euid,
            allow_isolated, txn->sender_pid))
            return -1;
        break;

    case SVC_MGR_LIST_SERVICES: {
        uint32_t n = bio_get_uint32(msg);
        //列举所有服务
        if (!svc_can_list(txn->sender_pid, txn->sender_euid)) {
            ALOGE("list_service() uid=%d - PERMISSION DENIED\n",
                    txn->sender_euid);
            return -1;
        }
        si = svclist;
        while ((n-- > 0) && si)
            si = si->next;
        if (si) {
            bio_put_string16(reply, si->name);
            return 0;
        }
        return -1;
    }
    default:
        ALOGE("unknown code %d\n", txn->code);
        return -1;
    }

    bio_put_uint32(reply, 0);
    return 0;
}

code 为 PING_TRANSACTION的话直接返回 0。 

SVC_MGR_GET_SERVICE 与 SVC_MGR_CHECK_SERVICE 则找到此 service 并返回 handle 值。

SVC_MGR_ADD_SERVICE 则是注册service。

SVC_MGR_LIST_SERVICES 是获取列表。

SurfaceFlinger

接下来了解下其他的 Server 怎么注册到 ServiceManager 中的,用 SurfaceFlinger 为例,它的工作内容主要包括合成的创建和管理、Vsync 信号的处理。同样是 init 进程创建出来的,运行在独立的 SurfaceFlinger 进程中。surfaceflinger.rc 在 \frameworks\native\services\surfaceflinger\ 下,同样 SurfaceFlinger.cpp 也在相同目录下。

service surfaceflinger /system/bin/surfaceflinger
    class core animation
    user system

SurfaceFlinger 并没有 main 函数,它是在 main_surfaceflinger.cpp 中开始启动的(在 /system/bin/surfaceflinger 中定义),看下它的 main 函数

int main(int, char**) {
   
    //设定surfaceflinger进程的binder线程池个数上限为4,并启动binder线程池
    ProcessState::self()->setThreadPoolMaxThreadCount(4);

    
    //实例化surfaceflinger
    sp<ProcessState> ps(ProcessState::self());
    ps->startThreadPool();

    // instantiate surfaceflinger
    sp<SurfaceFlinger> flinger = new SurfaceFlinger();
    
    //初始化
    flinger->init();

    //将服务注册到Service Manager
    sp<IServiceManager> sm(defaultServiceManager());
    sm->addService(String16(SurfaceFlinger::getServiceName()), flinger, false);

    // publish GpuService

    flinger->run();

    return 0;
}

先了解下 ProcessState::self()

ProcessState.h中
class ProcessState : public virtual RefBase
{
public:
    static  sp<ProcessState>    self();// 单例模式,获取实例  
}

sp<ProcessState> ProcessState::self()
{
    if (gProcess != NULL) {
        return gProcess;
    }
    gProcess = new ProcessState("/dev/binder");
    return gProcess;
}

先检查是否存在一个已经实例化的 ProcessState,否则创建一个,所以获取 ProcessState 对象,需要通过这个 self 方法。在参数中我们看到了 binder 驱动的路径。

ProcessState 的构造函数

进入构造函数
ProcessState::ProcessState(const char *driver)
    : mDriverName(String8(driver))
    , mDriverFD(open_driver(driver))
    , mVMStart(MAP_FAILED)
    , mThreadCountLock(PTHREAD_MUTEX_INITIALIZER)
    , mThreadCountDecrement(PTHREAD_COND_INITIALIZER)
    , mExecutingThreadsCount(0)
    , mMaxThreads(DEFAULT_MAX_BINDER_THREADS)
    , mStarvationStartTimeMs(0)
    , mManagesContexts(false)
    , mBinderContextCheckFunc(NULL)
    , mBinderContextUserData(NULL)
    , mThreadPoolStarted(false)
    , mThreadPoolSeq(1)
{
    if (mDriverFD >= 0) {
        // mmap the binder, providing a chunk of virtual address space to receive transactions.
        mVMStart = mmap(0, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0);
        if (mVMStart == MAP_FAILED) {
            // *sigh*
            ALOGE("Using /dev/binder failed: unable to mmap transaction memory.\n");
            close(mDriverFD);
            mDriverFD = -1;
            mDriverName.clear();
        }
    }

    LOG_ALWAYS_FATAL_IF(mDriverFD < 0, "Binder driver could not be opened.  Terminating.");
}

其中open_driver函数
static int open_driver(const char *driver)
{
    int fd = open(driver, O_RDWR | O_CLOEXEC);
    if (fd >= 0) {
        
        status_t result = ioctl(fd, BINDER_VERSION, &vers);
        
        size_t maxThreads = DEFAULT_MAX_BINDER_THREADS;
        result = ioctl(fd, BINDER_SET_MAX_THREADS, &maxThreads);
       
    } else {
        ALOGW("Opening '%s' failed: %s\n", driver, strerror(errno));
    }
    return fd;
}

我们看到与 Binder 驱动紧密相关的两个函数一个是 open_driver(),另一个是下面的 mmap() 都实现了,这样也就是最终打开了 Binder 结点以及进行了内存块的映射。也就是说在 SurfaceFlinger 的进程中调用了 ProcessState::self(),打开了 binder 驱动并映射了内存。

我们继续回到 main_surfaceflinger 的 main 中,之后 SurfaceFlinger 的启动流程不是我们这篇的重点,这里简单带过。先创建 binder 线程池,在实例化 SurfaceFlinger(定义在 SurfaceFlinger.cpp 中),调用 init 函数初始化。

这里进入 startThreadPool 先了解下与 ProcessState 类似的 IPCProcessState。

void ProcessState::startThreadPool()
{
    if (!mThreadPoolStarted) {
        mThreadPoolStarted = true;
        spawnPooledThread(true);
    }
}
void ProcessState::spawnPooledThread(bool isMain)
{
    if (mThreadPoolStarted) {
        String8 name = makeBinderThreadName();
        sp<Thread> t = new PoolThread(isMain);
        t->run(name.string());
    }
}

其实这里就是创建一个线程 PoolThread,而 PoolThread 是一个继承于 Thread 的类。所以调用 t->run() 之后相当于调用 PoolThread 类的 threadLoop() 函数,我们来看看 PoolThread 类的 threadLoop 线程函数。  

class PoolThread : public Thread
{
public:
    explicit PoolThread(bool isMain)
        : mIsMain(isMain)
    {
    }
    
protected:
    virtual bool threadLoop()
    {
        IPCThreadState::self()->joinThreadPool(mIsMain);
        return false;
    }
    
    const bool mIsMain;
};

我们知道:进程调用 spawnPoolThread() 创建了一个线程,执行 joinThreadPool(),而主线程也是调用这个函数。唯一区别
是参数 isMain,主线程调用的 joinThreadPool(true),创建的线程调用的是 jointThreadPool(false)。

看下 IPCThreadState  这个类

IPCThreadState* IPCThreadState::self()
{
// 判断gHaveTLS是true还是false,字面意思也可以知道是用来标记有无TLS(Thread Loacal Storage)的
    if (gHaveTLS) {
//当执行完第一次之后,再次运行的时候就已经有IPCThreadState实例,只需要获取就可以使用
restart:
        const pthread_key_t k = gTLS;
        //如果存在TLS,则根据key:gTLS去获取存储指向IPCThreadState对象的指针值,
        //这是一个一对多的关系,所有线程使用相同的key,但是其每个线程存储的值(IPCThreadState对象)
        //是线程私有的,每个线程都有一个这样的对象
        IPCThreadState* st = (IPCThreadState*)pthread_getspecific(k);
        if (st) return st;
        //如果此线程不存在这样的key:gTLS对应的值,则创建一个IPCThreadState新对象,
        //并在其构造函数中添加进key:gTLS对应的线程局部空间中TLS(thread local storage)
        return new IPCThreadState;
    }

    if (gShutdown) {
        ALOGW("Calling IPCThreadState::self() during shutdown is dangerous, expect a crash.\n");
        return NULL;
    }

    pthread_mutex_lock(&gTLSMutex);
    if (!gHaveTLS) {
    //初始的gHaveTLS的值false,所以第一次调用的时候,会执行这里的代码
    //随后将gHaveTLS设置为true
        //创建基于key:gTLS的线程局部空间
        int key_create_value = pthread_key_create(&gTLS, threadDestructor);
        if (key_create_value != 0) {
            pthread_mutex_unlock(&gTLSMutex);
            ALOGW("IPCThreadState::self() unable to create TLS key, expect a crash: %s\n",
                    strerror(key_create_value));
            return NULL;
        }
        gHaveTLS = true;
    }
    pthread_mutex_unlock(&gTLSMutex);
    goto restart;
}

首先 创建一个类型为 pthread_key_t 类型的变量 gTLS,第一次进来 gHaveTLS 为 false,再调用  pthread_key_create() 来创建该变量。该函数有两个参数,第一个参数就是上面声明的 pthread_key_t 变量,第二个参数是一个清理函数,用来在线程释放该线程存储的时候被调用。该函数指针可以设成 NULL ,这样系统将调用默认的清理函数。之后进来 gTLS 为 true,如需取出所存储的值,调用 pthread_getspecific() 。该函数的参数为前面提到的 pthread_key_t 变量 gTLS,该函数返回 IPCThreadState 类型的值。

void IPCThreadState::threadDestructor(void *st)
{
        IPCThreadState* const self = static_cast<IPCThreadState*>(st);
        if (self) {
                self->flushCommands();
#if defined(__ANDROID__)
        if (self->mProcess->mDriverFD > 0) {
            ioctl(self->mProcess->mDriverFD, BINDER_THREAD_EXIT, 0);
        }
#endif
                delete self;
        }
}

 清理函数 threadDestructor 调用 IPCThreadState 自身,IPCThreadState 构造函数

IPCThreadState::IPCThreadState()
    : mProcess(ProcessState::self()),
      mStrictModePolicy(0),
      mLastTransactionBinderFlags(0)
{
    pthread_setspecific(gTLS, this);//在此往key:gTLS添加IPCThreadState对象
    clearCaller();
    mIn.setDataCapacity(256);//初始化线程私有的数据输入空间大小
    mOut.setDataCapacity(256);//初始化线程私有的数据输出空间大小
}

这里调用 pthread_setspcific(),将自身 this 与 gTLS 关联 。该函数有两个参数,第一个为前面声明的 pthread_key_t 变量,第二个为 void* 变量,这样你可以存储任何类型的值。

理解了 ProcessState 和 IPCProcessState,接着看下 IPCThreadState::self()->joinThreadPool 函数

void IPCThreadState::joinThreadPool(bool isMain)
{    
    mOut.writeInt32(isMain ? BC_ENTER_LOOPER : BC_REGISTER_LOOPER);

    status_t result;
    do {
        processPendingDerefs();
        //此函数为joinThreadPool()函数的主要任务过程
        result = getAndExecuteCommand();

    } while (result != -ECONNREFUSED && result != -EBADF);
    mOut.writeInt32(BC_EXIT_LOOPER);
    talkWithDriver(false);
}

它内部和 ServiceManager 的 binder_loop 类似,循环读取数据。getAndExecuteCommand() 这篇就不跟了。

接下来重点在回到 SurfaceFlinger 的 main 中,看下 defaultServiceManager,回到 binder 目录下 IServiceManager.cpp 中

sp<IServiceManager> defaultServiceManager()
{
    if (gDefaultServiceManager != NULL) return gDefaultServiceManager;

    {
        AutoMutex _l(gDefaultServiceManagerLock);
        while (gDefaultServiceManager == NULL) {
            gDefaultServiceManager = interface_cast<IServiceManager>(
                ProcessState::self()->getContextObject(NULL));
            if (gDefaultServiceManager == NULL)
                sleep(1);
        }
    }

    return gDefaultServiceManager;
}

判断 gDefaultServiceManager 是否为 null,不为 null 直接返回,为 null 调用 getContextObject 获取。如果获取出来还是 null,sleep(1) 之后再获取。

getContextObject,在 ProcessState.cpp 中

sp<IBinder> ProcessState::getContextObject(const sp<IBinder>& /*caller*/)
{
    return getStrongProxyForHandle(0);
}

sp<IBinder> ProcessState::getStrongProxyForHandle(int32_t handle)
{
    sp<IBinder> result;

    AutoMutex _l(mLock);

    handle_entry* e = lookupHandleLocked(handle);

    if (e != NULL) {

        IBinder* b = e->binder;
        if (b == NULL || !e->refs->attemptIncWeak(this)) {
            if (handle == 0) {

                Parcel data;
                status_t status = IPCThreadState::self()->transact(
                        0, IBinder::PING_TRANSACTION, data, NULL, 0);
                if (status == DEAD_OBJECT)
                   return NULL;
            }

            b = new BpBinder(handle); 
            e->binder = b;
            if (b) e->refs = b->getWeakRefs();
            result = b;
        } else {
          
            result.force_set(b);
            e->refs->decWeak(this);
        }
    }

    return result;
}

这里可以看到它将 handle 值为 0 的 IBinder 对象返回了。这里的句柄 0 被赋予了特殊意义;它就是 ServiceManager 的句柄,在 Binder 驱动中,若获取到句柄的值是 0,则会将其目标当作是 ServiceManager。

ProcessState::handle_entry* ProcessState::lookupHandleLocked(int32_t handle)
{
    const size_t N=mHandleToObject.size();
    if (N <= (size_t)handle) {
        handle_entry e;
        e.binder = NULL;
        e.refs = NULL;
        status_t err = mHandleToObject.insertAt(e, N, handle+1-N);
        if (err < NO_ERROR) return NULL;
    }
    return &mHandleToObject.editItemAt(handle);
}

lookupHandleLocked(),是在矢量数组 mHandleToObject 中查找是否有句柄为 handle 的 handle_entry 对象。有的话,则返回该 handle_entry 对象;没有的话,则新建 handle 对应的 handle_entry,并将其添加到矢量数组 mHandleToObject 中,然后再返回。mHandleToObject 是用于保存各个 IBinder 代理对象的矢量数组,它相当于一个缓冲。

这里找到 0 的 handle_entry,显然这里 handle_entr 不为 null,且为 0。这里 code 为 PING_TRANSACTION ,如果 ServiceManager 启动了返回 0,没有启动返回 null。当 Binder 驱动已启动,ping 通信是能够成功的。之后调用 IPCThreadState::self()->transact() 尝试去和 Binder驱动通信,接着,新建 BpBinder 对象,并赋值给 e->binder。然后,将该 BpBinder 对象返回。

又由于 interface_casti<IServiceManager>(ProcessState::self()->getContextObject(NULL));所以其代码在 IInterface.h 中

template<typename INTERFACE>
inline sp<INTERFACE> interface_cast(const sp<IBinder>& obj)
{
    return INTERFACE::asInterface(obj);
}

interface_cast 本身是一个模板,IServiceManager.h 中定义

class IServiceManager : public IInterface
{
public:
    DECLARE_META_INTERFACE(ServiceManager)
}

template<typename INTERFACE>
status_t getService(const String16& name, sp<INTERFACE>* outService)
{
    const sp<IServiceManager> sm = defaultServiceManager();
    if (sm != NULL) {
        *outService = interface_cast<INTERFACE>(sm->getService(name));
        if ((*outService) != NULL) return NO_ERROR;
    }
    return NAME_NOT_FOUND;
}

 ProcessState::self()->getContextObject(NULL) 返回一个 BpBinder 对象。interface_cast<IServiceManager>(BpBinder);则是创建一个新的 BpServiceManager 对象并返回,且创建 BpServiceManager 时把 BpBinder 作为其参数,结果是把 BpBinder 对象赋值给其基类BpRefBase 的 mRemote 来保存。

BpServiceManager:

class BpServiceManager : public BpInterface<IServiceManager>
{
public:
    explicit BpServiceManager(const sp<IBinder>& impl)
        : BpInterface<IServiceManager>(impl)
    {
    }
}

实则拿到了 BpServiceManager 这样一个 ServiceManager 的代理对象。

addService

接下来就是 SurfaceFlinger 中 addService 了,它调用了 ServiceManager 的 addService,传入自身名字、自身实例、false。当然是再 SurfaceFlinger 进程中调用的。

    virtual status_t addService(const String16& name, const sp<IBinder>& service,
            bool allowIsolated)
    {
        Parcel data, reply;
        data.writeInterfaceToken(IServiceManager::getInterfaceDescriptor());
        data.writeString16(name);
        data.writeStrongBinder(service);
        data.writeInt32(allowIsolated ? 1 : 0);
        status_t err = remote()->transact(ADD_SERVICE_TRANSACTION, data, &reply);
        return err == NO_ERROR ? reply.readExceptionCode() : err;
    }

这里就和我们之前 AIDL 相似了,不过这里调用 data.writeStrongBinder,而不是 data.writeToParcel,之后 remote()->transact 方法将 code 为 ADD_SERVICE_TRANSACTION 发出去。所以这里调到了 BpBinder的transact 函数

status_t BpBinder::transact(
    uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)
{
    if (mAlive) {
        status_t status = IPCThreadState::self()->transact(
            mHandle, code, data, reply, flags);
        if (status == DEAD_OBJECT) mAlive = 0;
        return status;
    }

    return DEAD_OBJECT;
}

调用到自身线程的 IPCThreadState::self() 的 transact 函数。这里就此打住先。

SurfaceFlinger 调用 ServiceManager 的 BpBinder 的 transact 函数,自然 ServiceManager 的 BBinder 会在 onTransact 中收到。

IServiceManager 中枚举

    enum {
        GET_SERVICE_TRANSACTION = IBinder::FIRST_CALL_TRANSACTION,
        CHECK_SERVICE_TRANSACTION,
        ADD_SERVICE_TRANSACTION,
        LIST_SERVICES_TRANSACTION,
    };

        FIRST_CALL_TRANSACTION  = 0x00000001,

中枚举

enum {
    PING_TRANSACTION  = B_PACK_CHARS('_','P','N','G'),
    SVC_MGR_GET_SERVICE = 1,
    SVC_MGR_CHECK_SERVICE,
    SVC_MGR_ADD_SERVICE,
    SVC_MGR_LIST_SERVICES,
};

可以看看到 ADD_SERVICE_TRANSACTION 与 SVC_MGR_ADD_SERVICE 一致。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值