自学内容网 自学内容网

Linux——进程控制

前言:大佬写博客给别人看,菜鸟写博客给自己看,我是菜鸟

1.进程终止

1.1进程退出场景

先前我们说过,main函数并不是进程的开始,进程的开始是从start(目前尚未学习)开始,并调用main函数,那么main函数的返回值代表什么意思?

main函数不同返回值所代表的意思

①:0     → 代码运行完毕,结果正确

②:非0 → 代码运行完毕,结果不正确

1.2常见进程退出的方法

正常终止

①:从main返回

②:调用exit()   →库函数,进程退出时会进行缓冲区的刷新

③:调用_exit() →系统调用,进程退出的时候不会进行缓冲区的刷新

注1:正常终止的情况下,可以通过 echo $? 查看最近一次进程的退出码

注2:exit和_exit() 括号内的值可以任意指定,你想写什么就写什么

注3:只有操作系统能够决定一个进程的生死,因此库函数exit()本质是底层封装了_exit

异常退出

ctrl+c

2.进程等待

2.1进程等待的必要性

僵尸进程:当子进程结束时,父进程不管不顾则会进入僵尸状态。僵尸状态可能会存在内存泄露的问题。

注1:处于该状态下的子进程等待着父进程去处理它

注2:其实更重要的是回收子进程的资源,子进程的退出信息有时候根本不重要。

:那么父进程是如何回收子进程资源,并获取子进程退出信息的?

:每个子进程退出时,都会有对应的退出码,退出码会记录到PCB当中,父进程可以调用waitpid() 去找子进程的对应信息,并按位操作传递到父进程对应的地址空间上

:在源码中我们可以看到退出码以及退出信号(这个稍后论述)

2.2进程等待的方法

wait

#include<sys/types.h>
#include<sys/wait.h>

pid_t wait(int* status);

返回值

成功时,返回被等待进程的pid

失败是,返回 -1

括号内参数

输出型参数,获取子进程的退出状态,不关心则可以设置为NULL

waitpid

pid_ t waitpid(pid_t pid, int *status, int options);

返回值

当正常返回的时候,waitpid返回收集到的子进程的进程ID;
如果选项(options)设置为WNOHANG,而调用中waitpid() 发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

pid
Pid= -1 , 等待任⼀个⼦进程。与 wait 等效。
Pid> 0. 等待其进程 ID pid 相等的⼦进程。

status: 输出型参数(为NULL时表示不关心子进程状态)
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常出)
WEXITSTATUS(status): WIFEXITED非 零,提取子进程退出码。(查看进程的退出码)
options:默认为 0 ,表示阻塞等待
WNOHANG: pid 指定的子进程没有结束,则 waitpid() 函数返回 0 ,不予以等待。
若正常结束,则返回该子进程的ID

errno:strerror(errno); 可以显示具体错误信息;return errno;  可以直接返回错误的值。

WNOHANG:非阻塞调用→父进程不断调用函数去访问子进程获取子进程信息,调用一次后立马返回,又叫非阻塞轮询,通过循环完成。在非阻塞(NO BLOCK)状态下,父进程在调用完后,可以去执行其他进程,而对于阻塞状态而言,当父进程调用函数访问子进程时,会一直处于等待子进程响应的状态,例如scanf()函数。

2.3获取子进程的status

wait和waitpid,都有一个status参数,该参数时一个输出型参数,由操作系统填充。

如果传递为NULL,表示不关心子进程的退出状态信息

如果非NULL,操作系统会根据该参数,将子进程的推出信息反馈给父进程

但是status不能简单的当作整型来看待

我们来看下述代码

int main()
{
    int i = 4;                                                                                                                                                                                                                                 
    exit(1);                      
                                     
    return 0;                     
}  

当执行上述代码,并通过 echo $? 指令查看退出码时,我们可以得到以下结果:

可以看到,退出码就是exit 括号中的参数。

而当我们写下如下图代码并执行时,却得到了意想不到的结果。

int main()
{
    int i = 4;   
    i /= 0;                                                                                                                                                                                                                              
    exit(1);                      
                                     
    return 0;                     
}  

以下为status的位图

status为整型,一共4个字节,每个字节8个位,其中,高16位不用,低16位中高8位为退处状态(退出码),第8位为core dump标志(此处先不做论述),后7位为信号位。

我们可以通过按位与的方式来分别获得status中的退出码和信号

退出状态:status&0x7F

信号:(status>>8)&0xFF

:当进程正常终止时,信号部分默认为0,只看退出码状态;而当进程意外终止时,此时退出码已经不重要了,全为0,只看终止信号部分。

3.进程替换

前言:进程替换是一个系统级别的概念

一个父进程执行fork()之后,就会有一个子进程,二者各自执行父进程的代码,那如果子进程想执行一个全新的程序呢?此时就有了进程替换这个概念。

进程替换函数

#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);

3.1命名解释

补充1

子进程在替换程序的过程中,并没有创建新的进程,而是用新的程序的代码和数据覆盖式的替换当前数据和代码,即进程替换前后的pid不变

:我们可以通过代码来验证

:为什么进程发生替换后没有影响父进程?

:1.进程具有独立性 2.子进程在创建的时候,会继承父进程的数据和代码,当子进程修改参数时,系统会发生写实拷贝,给数据开辟一块新的物理空间,虚拟地址不变,并改变页表中的映射关系。对于进程替换,则数据和代码都要发生写实拷贝。

补充2

可以看到 部分exec* 函数中还有关环境变量表的参数,如果在子进程中,自己设置了环境变量表,那么在调用 exec*函数时,子进程中新修改环境变量表会覆盖原先的环境变量表。

:子进程是如何继承父进程的环境变量表的?

:通过 exec* 函数。对于没有环境变量表参数的 exec* 函数,它本质上会去调用另一个exec*函数,而该函数中包含 环境变量表参数,且这个参数默认为 **environ(全局指针),子进程在创建时,这个全局指针会默认指向父进程的环境变量表;而对于有环境变量表的 exec* 函数,则如补充2中说明的那样对父进程的环境变量表进行覆盖,也可以通过 putenv("字符串") 添加新的环境变量,再将环境变量参数设置为 **environ,来实现不覆盖原环境变量表的基础上添加新的环境变量。

认知1

①:exec* 接口相当于加载器,bash创建父进程,父进程创建子进程,子进程再通过 exec* 去执行别的进程

②:可以通过exec* 接口去调用其他语言写的程序,本质上不是调用程序,是调用解释器,不管什么语言,只要在计算器中运行,都是进程,只要是进程都可以替换

4.简单实现一个shell

☆☆☆前言:代码本身没有实际意义,但是可以锻炼编码能力,此处包含很多先前学过的东西

4.1思路

思路

①:编写命令行提示符,包含用户名USER、主机名HOSTNAME、当前路径

②:获取用户输入的命令,通过字符串输入,例如"ls -l -a"

③:命令分析,将字符串"ls -l -a" 转为 "ls" "-l" "-a"

④:判断是否为内建命令,cd echo等

⑤:执行,创建子进程,调用 execvp() 函数进行进程替换

4.2代码实现

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# "

// 下面是shell定义的全局变量

#define MAXARGC 128
char *g_argv[MAXARGC];
int g_argc = 0; 

// 2. 环境变量表
#define MAX_ENVS 100
char *g_env[MAX_ENVS];
int g_envs = 0;

// for test
char cwd[1024];
char cwdenv[1024];

// last exit code
int lastcode = 0;

const char *GetUserName()
{
    const char *name = getenv("USER");
    return name == NULL ? "None" : name;
}

const char *GetHostName()
{
    const char *hostname = getenv("HOSTNAME");
    return hostname == NULL ? "None" : hostname;
}

const char *GetPwd()
{
    //const char *pwd = getenv("PWD");
    const char *pwd = getcwd(cwd, sizeof(cwd));
    if(pwd != NULL)
    {
        snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);
        putenv(cwdenv);
    }
    return pwd == NULL ? "None" : pwd;
}

const char *GetHome()
{
    const char *home = getenv("HOME");
    return home == NULL ? "" : home;
}

void InitEnv()
{
    extern char **environ;
    memset(g_env, 0, sizeof(g_env));
    g_envs = 0;

    //本来要从配置文件来
    //1. 获取环境变量
    for(int i = 0; environ[i]; i++)
    {
        // 1.1 申请空间
        g_env[i] = (char*)malloc(strlen(environ[i])+1);
        strcpy(g_env[i], environ[i]);
        g_envs++;
    }
    g_env[g_envs++] = (char*)"HAHA=for_test"; //for_test
    g_env[g_envs] = NULL;

    //2. 导成环境变量
    for(int i = 0; g_env[i]; i++)
    {
        putenv(g_env[i]);
    }
    environ = g_env;
}

std::string DirName(const char *pwd)
{
#define SLASH "/"
    std::string dir = pwd;
    if(dir == SLASH) return SLASH;
    auto pos = dir.rfind(SLASH);
    if(pos == std::string::npos) return "BUG?";
    return dir.substr(pos+1);
}

void MakeCommandLine(char cmd_prompt[], int size)
{
    //snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), DirName(GetPwd()).c_str());
    snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), GetPwd());
}

void PrintCommandPrompt()
{
    char prompt[COMMAND_SIZE];
    MakeCommandLine(prompt, sizeof(prompt));
    printf("%s", prompt);
    fflush(stdout);
}

bool GetCommandLine(char *out, int size)
{
    // ls -a -l => "ls -a -l\n" 字符串
    char *c = fgets(out, size, stdin);
    if(c == NULL) return false;
    out[strlen(out)-1] = 0; // 清理\n
    if(strlen(out) == 0) return false;
    return true;
}

// 3. 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
bool CommandParse(char *commandline)
{
#define SEP " "
    g_argc = 0;
    // 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
    g_argv[g_argc++] = strtok(commandline, SEP);
    while((bool)(g_argv[g_argc++] = strtok(nullptr, SEP)));
    g_argc--;
    return true;
}


bool Cd()
{
    // cd argc = 1
    if(g_argc == 1)
    {
        std::string home = GetHome();
        if(home.empty()) return true;
        chdir(home.c_str());
    }
    else
    {
        std::string where = g_argv[1];
        // cd - / cd ~
        if(where == "-")
        {
            // Todu
        }
        else if(where == "~")
        {
            // Todu
        }
        else
        {
            chdir(where.c_str());
        }
    }
    return true;
}

void Echo()
{
    if(g_argc == 2)
    {
        // echo "hello world"
        // echo $?
        // echo $PATH
        std::string opt = g_argv[1];
        if(opt == "$?")
        {
            std::cout << lastcode << std::endl;
            lastcode = 0;
        }
        else if(opt[0] == '$')
        {
            std::string env_name = opt.substr(1);
            const char *env_value = getenv(env_name.c_str());
            if(env_value)
                std::cout << env_value << std::endl;
        }
        else
        {
            std::cout << opt << std::endl;
        }
    }
}

bool CheckAndExecBuiltin()
{
    std::string cmd = g_argv[0];
    if(cmd == "cd")
    {
        Cd();
        return true;
    }
    else if(cmd == "echo")
    {
        Echo();
        return true;
    }
    else if(cmd == "export")
    {
    }
    else if(cmd == "alias")
    {
       // std::string nickname = g_argv[1];
       // alias_list.insert(k, v);
    }

    return false;
}                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               


int Execute()
{
    pid_t id = fork();
    if(id == 0)
    {
        //child
        execvp(g_argv[0], g_argv);
        exit(1);
    }

    // father
    pid_t rid = waitpid(id, nullptr, 0);
    (void)rid; // rid使用一下
    return 0;
}

int main()
{
    while(true)
    {
        // 1. 输出命令行提示符
        PrintCommandPrompt();

        // 2. 获取用户输入的命令
        char commandline[COMMAND_SIZE];
        if(!GetCommandLine(commandline, sizeof(commandline)))
            continue;

        // 3. 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
        CommandParse(commandline);
        //PrintArgv();

        // 4. 执行命令
        Execute();
    }
    return 0;
}

4.3代码分析

4.3.1全局变量

#define COMMAND_SIZE 1024    //用户能够输入的最多字符个数
#define FORMAT "[%s@%s %s]# "//用于后续代码的维护

#define MAXARGC 128          //命令行参数表的最大个数
char *g_argv[MAXARGC];       //命令行参数表
int g_argc = 0;              //命令行参数表中数据的个数

#define MAX_ENVS 100         //环境变量表数据最大个数
char *g_env[MAX_ENVS];       //环境变量表
int g_envs = 0;              //当前个数

int lastcode = 0;            //用于记录最后一次进程的退出码

4.3.2配置环境变量表

void InitEnv()
{
    extern char **environ;
    memset(g_env, 0, sizeof(g_env));
    g_envs = 0;

//1. 获取环境变量
    for(int i = 0; environ[i]; i++)
    {
        // 1.1 申请空间
        g_env[i] = (char*)malloc(strlen(environ[i])+1);
        strcpy(g_env[i], environ[i]);
        g_envs++;
    }
    g_env[g_envs++] = (char*)"HAHA=for_test"; //for_test
    g_env[g_envs] = NULL;
 //2. 导成环境变量
    for(int i = 0; g_env[i]; i++)
    {
        putenv(g_env[i]);
    }
    environ = g_env;
}

每个进程创建时,都需要配置环境变量表。

通过定义一个全局字符数组 g_env[] 来模拟环境变量表,字符数组存储的数据为字符指针,通过malloc()开辟strlen(environ[i])+1大小的空间,最后一位为\0,再通过strcpy()将原环境变量表中的数据拷贝到自己的环境变量当中,另外,通过遍历字符数组,可以实现环境变量表参数的增加

:环境变量表最后一位一定要置为NULL

最后将自己的环境变量表导入到系统的环境变量表,同时更新environ

:是否可以删除第二步,直接执行 environ = g_env?

:不可以!这样做只是将全局指针指向了一个自定义环境变量表,但是进程中实际的环境变量表并没有更新,后续执行还是按照原先的环境变量表来。

基础复习1

environ:二级字符指针,environ[i] 为一级字符指针,指向字符串的首元素地址

char str[]:字符数组,单个数据为 'a' ,数据类型为 char

char* str[]:字符指针数组,单个数据为 "a",数据类型为 char* 的指针,指向字符首元素地址

4.3.3命令行提示符

void MakeCommandLine(char cmd_prompt[], int size)
{
    //snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), DirName(GetPwd()).c_str());
    snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), GetPwd());
}

void PrintCommandPrompt()
{
    char prompt[COMMAND_SIZE];
    MakeCommandLine(prompt, sizeof(prompt));
    printf("%s", prompt);
    fflush(stdout);
}

标准的命令行提示符如图所示:

包含:用户名、主机名、当前路径(最后一个路径)。因此我们需要创建一个字符数组,并调用getenv()函数去获得相应数据,最后通过snprintf()函数,将得到的数据格式化管理。

学习:int snprintf(char *str, size_t size, const char *format, ...);

功能:将内容格式化传入到目标字符数组内

size 保证了传入的大小,最大是 size-1 因为得保证最后一个字符为\0;
format 格式化字符串,类似于printf("format");中的字符串

:snprintf()不会将结果打印在显示器上,他不是printf();

当自定义函数MakeCommandLine()执行完毕后,此时 prompt数组中的数据已经格式化保存完毕,再通过printf()函数打印字符串即可实现命令行提示符的输出,如图所示:

基础复习2:当数组作为实参被传递时,形参既可以用相应数组接收(必须加[] ),也可以用相应的指针接收

4.3.4获取用户输入的命令

bool GetCommandLine(char *out, int size)
{
    // ls -a -l => "ls -a -l\n" 字符串
    char *c = fgets(out, size, stdin);
    if(c == NULL) return false;
    out[strlen(out)-1] = 0; // 清理\n
    if(strlen(out) == 0) return false;
    return true;
}

int main()
{
    while(true)
    {
        // 1. 输出命令行提示符
        // 2. 获取用户输入的命令
        char commandline[COMMAND_SIZE];
        if(!GetCommandLine(commandline, sizeof(commandline)))
            continue;
        // 3. 命令行分析 
        // 4. 执行命令
    }
    return 0;
}

我们需要将用户从键盘上输入的指令存放到字符数组commandline当中,为此调用自定义函数GetCommandLine(),将字符数组和大小设为实参,同时在自定义函数中调用fgets()函数来读取键盘输入,并返回给c。

:为什么要执行out[strlen(out)-1] = 0; ?

:用户在输入完指令时,会按回车来进行确认,因此这么做是为了将最后一个字符"\n"排除。排除后若strlen(out)仍为零,说明此时没有输入任何指令

当commandline()函数返回值为:false时,说明此时指令输入失败,取反执行 continue; 跳过后续命令重新开始该循环

基础复习3:char *fgets(char *str, int n, FILE *stream);

功能:用于从文件或标准输入中读取一行数据,并将数据的首地址返回。

基础复习3:size_t strlen ( const char * str );

统计一个字符串的长度,直至遇到\0为止

基础复习3:continue;

跳过本轮循环,执行下一轮循环

4.3.5命令行分析

bool CommandParse(char *commandline)
{
#define SEP " "
    g_argc = 0;
    g_argv[g_argc++] = strtok(commandline, SEP);
    while((bool)(g_argv[g_argc++] = strtok(nullptr, SEP)));
    g_argc--;//因为是后置++,多加了一次,因此需要--
    return true;
}

命令行分析的核心即为:将 "ls -a -l" -> "ls" "-a" "-l",并保存在全局变量g_argv[]内。

每一次进行命令分析时,都要刷新g_argc,确保下一次能够成功解析命令。

学习:char * strtok ( char * str, const char * delimiters );

功能:将一串字符串按指定分隔符进行分割

例如:字符串"hello,world" 按 ","进行分割,就得到了hello,并将字符串首地址返回。

strtok(nullptr,SEP)表面不是第一次调用strtok,那么strtok()函数会从上一次分割的位置开始向后查找满足条件的位置,以此循环,直至结束。

该函数的功能就是将用户输入的字符串指令一一分割存储到g_argv[]内

4.3.6判断是否为内建命令

bool CheckAndExecBuiltin()
{
    std::string cmd = g_argv[0];
    if(cmd == "cd")
    {
        Cd();
        return true;
    }
    else if(cmd == "echo")
    {
        Echo();
        return true;
    }
   
    return false;
}

内建命令:cd、echo等这些都被称为内建命令。

通过一个string类 cmd 来接收命令行参数表中首个数据,并判断是否为内建命令,若是则执行相应语句。

4.3.6.1 cd命令
bool Cd()
{
    // cd argc = 1
    if(g_argc == 1)
    {
        std::string home = GetHome();
        if(home.empty()) return true;
        chdir(home.c_str());
    }
    else
    {
        std::string where = g_argv[1];
        // cd - / cd ~
        if(where == "-")
        {
            // Todu
        }
        else if(where == "~")
        {
            // Todu
        }
        else
        {
            chdir(where.c_str());
        }
    }
    return true;
}

目前我们正在执行第四步中的Cd()函数,此时:

①:g_argv[]中存储的是用户输入的指令(已解析)

②:g_argc 中存储着当前命令行参数表中指令个数

其次,cd指令有:

①:cd 回到家目录

②:cd /指定目录

③:cd - 和 cd ~ 这里不做考虑

于是,思路就清楚了,通过判断 g_argc的个数,来编写代码:

👉:为1,说明 g_argv[] 中只有一条指令 cd,即返回家目录,通过调用GetHome()(内部封装了getenv("HOME"))得到家目录,再通过chdir库函数进入到家目录

👉:不为1,说通过chdir库函数直接进入命令行参数表中的路径

4.3.6.2 ehco命令
void Echo()
{
    if(g_argc == 2)
    {
        // echo "hello world"
        // echo $?
        // echo $PATH
        std::string opt = g_argv[1];
        if(opt == "$?")
        {
            std::cout << lastcode << std::endl;
            lastcode = 0;
        }
        else if(opt[0] == '$')
        {
            std::string env_name = opt.substr(1);
            const char *env_value = getenv(env_name.c_str());
            if(env_value)
                std::cout << env_value << std::endl;
        }
        else
        {
            std::cout << opt << std::endl;
        }
    }
}

对于echo而言

①:echo $? → 获取上一进程的退出码

②:echo $环境变量名(例如“HOME”)  → 获取指定环境变量名

③:echo 字符串 → 打印字符串

有了对cd代码的认识,echo编写就容易了,至少从思路上而言,可以看到对于echo而言,命令行参数表中一定有两个参数,因此我们只需对第二个参数进行分析判断,将第二个参数存储到 string类 opt 当中来进行if条件判断

echo $?

我们需要在Execute();中获取子进程的退出码,通过 WEXITSTATUS(status);获取后直接打印即可

部分代码如下:其中lastcode为全局变量

    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if(rid > 0)
    {
        lastcode = WEXITSTATUS(status);
    }

echo $:获取当前环境变量名

我们需要通过getenv() 来获得当前变量名,而 opt中第一个参数为$不需要,因此我们通过substr(1) 从第二个位置开始往后遍历,并返回给 env_name;再通过getenv();获取环境变量名,最后字符串

echo 字符串

直接打印即可

4.3.6.3 补充

我们需要对GetPwd做一次修整,当我们执行cd命令时,命令行提示符,即第①步时

此时如果使用getenv("PWD")会出错(因为PWD仍为原来环境变量表中的PWD),为此我们需要更新环境变量中的PWD,代码如下:

const char *GetPwd()
{
    //const char *pwd = getenv("PWD");
    const char *pwd = getcwd(cwd, sizeof(cwd));
    if(pwd != NULL)
    {
        snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);
        putenv(cwdenv);
    }
    return pwd == NULL ? "None" : pwd;
}

4.3.7执行

int Execute()
{
    pid_t id = fork();
    if(id == 0)
    {
        //child
        execvp(g_argv[0], g_argv);
        exit(1);
    }

    // father
    pid_t rid = waitpid(id, nullptr, 0);
    (void)rid; // rid使用一下
    return 0;
}

创建子进程,通过execvp来调用相应的指令

4.4总结:

虽然代码本身没有太多实际意义,但是可以加强自己代码能力

复习

传址or传值?数组or数组名?

当我们创建一个数组,例如 char arr[]; 那么arr就是数组名,数组名为地址,如果将数组名作为实参传递,那么传的就是地址,在形参中通过相应指针进行接收,就是传址调用,传址调用会改变原来数组内的数据

指令

char *getcwd(char *buf, size_t size);

功能:获得当前进程的工作目录,并将首地址存放到字符指针 buf 中

返回值

成功:指向当前工作工作路径的字符串

失败:返回NULL

int putenv(char *string);

功能:修改环境变量(没有添加,有就覆盖)

返回值

成功:0

失败:非零

int snprintf(char *str, size_t size, const char *format, ...);

功能:格式化数据,并将字符串的首地址放入到字符指针str中

:str必须经过初始化,否则会出现段错误

返回值

成功:0

失败:-1

int chdir(const char *path);

功能:更改当前工作路径

返回值

成功:0

失败:-1


原文地址:https://blog.csdn.net/m0_51952310/article/details/144187241

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!