当前位置: 首页 > news >正文

控制欲过强的Linux小进程

控制欲强?视奸?普通人那才叫视奸,您是皇帝,天下大事无一逃过您的耳目,您想看什么就看什么,臣怀疑他在朋友圈私养兵士,囤积枪甲,蓄意谋反,图谋皇位啊!

哈哈哈哈开个玩笑,这篇就主要讲讲Linux进程的控制吧~ 

fork( )

由于fork()之前也说过啦(从已存在进程中创建一个新进程:新进程为子进程,原进程为父进程),所以下面主要讲内核的操作,进程调用fork,当控制转移到内核中的fork代码后,内核做:

1.分配新的内存块和内核数据结构给子进程

2.将父进程部分数据结构内容拷贝至子进程

3.添加子进程到系统进程列表当中

4.fork返回,开始调度器调度

 当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以 开始它们自己的旅程:

#include <unistd.h>
#include<stdio.h>
int main(void)
{pid_t pid;printf("Before: pid is %d\n", getpid());if ((pid = fork()) == -1)perror("fork()"), exit(1);printf("After:pid is %d, fork return %d\n", getpid(), pid);sleep(1);return 0;
}

先来下个定义:

进程=内核的相关管理数据结构(task_struct + mm_struct + 页表)+ 代码和数据

已知fork函数的返回值是这样的:

子进程返回0

父进程返回子进程的pid

那为什么捏?

原因其实也很简单,爹得知道儿子名,杀掉他啊等待他啊,爹总要知道的(为了方便父进程对紫禁城进行标识,进而进行管理) 

进程具有独立性就在于紫禁城代码数据和父进程共享,但因为写时拷贝又不影响父进程

fork常规用法

一个父进程希望复制自己,使父子进程同时执行不同的代码段(父进程等待客户端请求,生成子 进程来处理请求)

一个进程要执行一个不同的程序(子进程从fork返回后,调用exec函数)

fork调用失败原因

系统中有太多的进程

实际用户的进程数超过了限制

进程终止

终止是在做什么

进程终止就是在释放曾经的代码和数据所占据的空间,也是在释放内核数据结构(task_struct,当进程状态是Z就要释放对应PCB)

终止三种情况

先来看两段代码: 

#include<stdio.h>
#include<unistd.h>int main()
{printf("hello world!\n");return 0;
}
#include<stdio.h>
#include<unistd.h>int main()
{printf("hello world!\n");return 100;
}

只有返回值不一样对吧,对?取内容会发现也不一样: 

 

 echo是内建命令,打印的都是bash内部的变量数据

?:父进程bash获取到的最近一个紫禁城的退出码 (0:成功,!0:失败)

退出码存在意义:告诉关心方(父进程)任务完成如何

因为成功的退出码就是0,而!0有很多,所以不同!0值一方面表示失败,一方面还表示失败的原因

可以这样打印下错误信息:

#include<stdio.h>
#include<unistd.h>
#include<string.h>int main()
{int errcode = 0;for (errcode = 0; errcode <= 255; errcode++){printf("%d:%s\n", errcode, strerror(errcode));}return 0;
}

 那么父进程知道紫禁城退出码因为点撒捏?

因为:!要知道紫禁城退出情况,正常退出了嘛,错误了嘛,错哪了呀,,,

错误码可以自己设定:

#include<stdio.h>
#include<unistd.h>
#include<string.h>
int Div(int x, int y)
{if (0 == y){return -1;}else{return x / y;}
}
int main()
{printf("%d\n",Div(-1,1));return 0;
}

但是这样没法判断是y==0导致返回错误码-1,还是本来的计算结果就是-1

所以可以这样改:

#include<stdio.h>
#include<unistd.h>
#include<string.h>//自定义枚举常量
enum
{Success = 0,Div_Zero,Mod_Zero,
};int exit_code = Success;int Div(int x, int y)
{if (0 == y){exit_code = Div_Zero;return -1;}else{return x / y;}
}
int main()
{printf("%d\n", Div(-1, 1));return exit_code;
}

还可以接着写接口补充错误信息:

#include<stdio.h>
#include<unistd.h>
#include<string.h>//自定义枚举常量
enum
{Success = 0,Div_Zero,Mod_Zero,
};int exit_code = Success;const char* CodeToErrString(int code)
{switch (code){case Success:return "Success";case Div_Zero:return "div zero!";case Mod_Zero:return "mod zero!";default:return "unknow error!";}
}int Div(int x, int y)
{if (0 == y){exit_code = Div_Zero;return -1;}else{return x / y;}
}
int main()
{printf("%d\n", Div(-1, 1));printf("%s\n", CodeToErrString(exit_code));return exit_code;
}

 来看看进程终止的三种情况吧:

1.代码跑完,结果正确

2.代码跑完,结果不正确(正确与否可通过进程退出码决定)

3.代码执行时,出现了异常,提前退出了(系统&&自定义退出码)

什么是崩溃?

就是编译运行的时候,操作系统发现你的进程做了不该做的是事,于是OS杀掉了你的进程 

那异常了退出码还有意义吗(肯定没有啊,作弊拿到60和作弊拿到100被抓没区别)

进程出现了异常,本质是因为进程收到了OS发给进程的信号

比如说,来上一份妇孺皆知的代码:

#include<stdio.h>
#include<unistd.h>
int main()
{while (1){printf("I am a process,pid:%d\n", getpid());}return 0;
}

这进程能一直运行下去,但是我们可以通过kill的方式干掉它:

kill -9 pid;

这进程没有出现异常,但是由于进程收到了OS发给进程的信号,所以进程不得不终止 

再来一瓶野指针:

#include<stdio.h>
#include<unistd.h>
int main()
{int* p = NULL;while (1){printf("I am a process,pid:%d\n", getpid());sleep(1);*p = 100;       //看好了小登中登老登,这是故意哒!}return 0;
}

在Linux中运行这段代码会发现出现段错误:Segmentation fault:

 

不嘻嘻 ,段错误,OS提前终止进程                              

我们通过观察进程退出的时候退出信号是多少就可以判断我们的进程为何异常了

判断流程:

1.先确认是否异常

2.不是异常就是代码跑完了,直接看退出码

 衡量一个进程退出,只需要两个数字:退出码,退出信号

进程退出时会把退出码和退出信号写入PCB(方便父进程知道)

如何进行终止

main函数return就表示进程终止啦(非main函数return,代表函数结束)

代码调用exit函数(头文件为stdlib.h)

exit(0);        //里面数字是return数

还有个东西叫_exit( )

和exit的区别就是,它在程序结束的时候并不会冲刷缓冲区

缓冲区必定在_exit()之上

exit在调用_exit前还做了其他工作:

1. 执行用户通过 atexit或on_exit定义的清理函数

2. 关闭所有打开的流,所有的缓存数据均被写入

3. 调用_exit

除了exit,return是一种更常见的退出进程方法。执行return n等同于执行exit(n)(调用main的运行时函数会将main的返回值当做 exit的参数)

进程等待

是什么 

任何子进程在退出的情况下,一般必须要被父进程进行等待 

为什么捏?

你想奥,如果进程在退出时,父进程不管不顾,退出进程,状态将会变成Z(僵尸状态),发生内存泄漏(进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程,就像是永远没办法叫醒一个装睡的人)

1.父进程通过等待,解决紫禁城退出的僵尸问题,回收系统资源(一定要考虑的)

2.获取紫禁城的退出信息知道紫禁城是什么原因退出的(可选功能)

怎么办

要来看两个可爱的函数:wait、waitpid

wait:

返回值:等待成功时,紫禁城pid

参数:等待任意一个紫禁城退出(是输出型的参数,获取紫禁城退出状态,不关心可以置NULL)

pid_t wait(int* status);

 上代码!

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>void ChildRun()
{int cnt = 5;while (cnt){printf("I am child process,pid:%d,ppid:%d\n", getpid(), getppid());sleep(1);cnt--;}
}int main()
{printf("I am father,pid:%d,ppid:%d\n", getpid(), getppid());pid_t id = fork();if (id == 0){ChildRun();printf("child quit ...\n");exit(0);}sleep(3);pid_t rid = wait(NULL);if (rid > 0){printf("wait success,rid:%d\n", rid);}sleep(3);printf("father quit\n");return 0;
}

一遍运行一边开监控脚本看看怎么个事: 

while :; do ps ajx | head -1 && ps ajx | grep myprocess | grep -v grep; sleep 1;done

可以看到紫禁城在被父进程回收前是处于僵尸状态的: 

 

 父进程在等待时候也没干其他事,只是等

给大家看看单核处理器小猫:

polo  tiu ~,橘域网链接已断开

如果紫禁城没有退出,父进程其实一直在进行阻塞等待

紫禁城本身就是软件,父进程本质是在等待某种软件条件就绪

阻塞等待?

怎么个事?

等待硬件or软件,本质都是数据结构对象

来康康waitpid:

关于这个就改一下就好:

pid_t rid = waitpid(-1, NULL, 0);

作用和上面的也一样(-1是在等任意一个的意思),等待紫禁城,等待到了哪个就返回哪个,那样的还准备俩函数干啥,别着急,这样就能等待特定的了,我是在等,可我在等的只是你:

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>void ChildRun()
{int cnt = 5;while (cnt){printf("I am child process,pid:%d,ppid:%d\n", getpid(), getppid());sleep(1);cnt--;}
}int main()
{printf("I am father,pid:%d,ppid:%d\n", getpid(), getppid());pid_t id = fork();if (id == 0){ChildRun();printf("child quit ...\n");exit(0);}sleep(3);pid_t rid = waitpid(id, NULL, 0);if (rid > 0){printf("wait success,rid:%d\n", rid);}sleep(3);printf("father quit\n");return 0;
}

也是可能失败的(但基本上不会失败):

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>void ChildRun()
{int cnt = 5;while (cnt){printf("I am child process,pid:%d,ppid:%d\n", getpid(), getppid());sleep(1);cnt--;}
}int main()
{printf("I am father,pid:%d,ppid:%d\n", getpid(), getppid());pid_t id = fork();if (id == 0){ChildRun();printf("child quit ...\n");exit(0);}sleep(3);pid_t rid = waitpid(id+1, NULL, 0);if (rid > 0){printf("wait success,rid:%d\n", rid);}else{printf("wait failed\n");}sleep(3);printf("father quit\n");return 0;
}

再来回看这个函数:

pid_t waitpid(pid_t pid, int status, int options);

返回值:

        当正常返回的时候waitpid返回收集到的子进程的进程ID(等待成功,紫禁城退出

        父进程回收成功)

        若返回值为0,那证明检测成功,但紫禁城并未退出,需要再次进行等待

        若设置了选项WNOHANG,调用中waitpid发现没有已退出的子进程可收集,则返回0        

        如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在

参数:

        Pid:

                Pid=-1:等待任一个子进程,与wait等效

                Pid>0:等待其进程ID与pid相等的子进程

        Status:

                WIFEXITED(status): 若为正常终止子进程返回的状态,则为真(查看进程是否是正常退出)

                WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码(查看进程的退出码)

        options:

                WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待

                若正常结束,则返回该子进程的ID

                若子进程已经退出,调用wait/waitpid时,

                wait/waitpid会立即返回,并且释放资源,获得子进程退出信息

                若在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞

                若不存在该子进程,则立即出错返回

 退出信息就退出码和退出信号啦,可是Status只有一个数哎(别猜了人家有特殊格式,可以当做位图看待,图中表示比特位):

退出码:0~255(最多就那么多)

这样可以直接看:

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>void ChildRun()
{int cnt = 5;while (cnt){printf("I am child process,pid:%d,ppid:%d\n", getpid(), getppid());sleep(1);cnt--;}
}int main()
{printf("I am father,pid:%d,ppid:%d\n", getpid(), getppid());pid_t id = fork();if (id == 0){ChildRun();printf("child quit ...\n");exit(49);}sleep(3);int status = 0;pid_t rid = waitpid(id, &status , 0);if (rid > 0){printf("wait success,rid:%d\n", rid);}else{printf("wait failed\n");}sleep(3);printf("father quit,status:%d,child quit code:%d,child quit signal:%d\n",status,(status>>8)&0xFF, status & 0x7F);return 0;
}

 退出后会发现是正常退出的:

上面的宏和这个位操作差不多,使用的话就是(结果是紫禁城退出码,想知道退出信号就自己去按位与去):

if(WIFEXITED(status))

很好,正和我意

那假如紫禁城死循环怎么办?

看看不就知道了:

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>void ChildRun()
{int cnt = 5;while (1){printf("I am child process,pid:%d,ppid:%d\n", getpid(), getppid());sleep(1);cnt--;}
}int main()
{printf("I am father,pid:%d,ppid:%d\n", getpid(), getppid());pid_t id = fork();if (id == 0){ChildRun();printf("child quit ...\n");exit(123);}sleep(3);int status = 0;pid_t rid = waitpid(id, &status , 0);if (rid > 0){printf("wait success,rid:%d\n", rid);}else{printf("wait failed\n");}sleep(3);printf("father quit,status:%d,child quit code:%d,child qiut signal:%d\n",status,(status>>8)&0xFF, status & 0x7F);return 0;
}

当然是爹一直等了,,,把紫禁城干掉回收

如果紫禁城异常怎么办?

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>void ChildRun()
{int cnt = 5;int* p = NULL;while (cnt){printf("I am child process,pid:%d,ppid:%d\n", getpid(), getppid());sleep(1);cnt--;}*p = 10;
}int main()
{printf("I am father,pid:%d,ppid:%d\n", getpid(), getppid());pid_t id = fork();if (id == 0){ChildRun();printf("child quit ...\n");exit(123);}sleep(3);int status = 0;pid_t rid = waitpid(id, &status , 0);if (rid > 0){printf("wait success,rid:%d\n", rid);}else{printf("wait failed\n");}sleep(3);printf("father quit,status:%d,child quit code:%d,child qiut signal:%d\n",status,(status>>8)&0xFF, status & 0x7F);return 0;
}

也看看:

可以发现直接挂了,退出信息告诉程序猿:赶紧回去查查你的代码,有bug!!! 

如果紫禁城没有退出,父进程在进行执行waitpid进行等待(等待某种条件发生,只不过如今的条件恰好是紫禁城退出),阻塞等待(进程阻塞,父进程什么事都没干)

但是现在我们使用的大部分进程都是阻塞板的,WNOHANG选项就是非阻塞等待,如果一直hang住什么都做不了,我们把这种情况叫做服务器宕机

讲个小故事来阐述这个故事吧:

从前有一只学生名为燃燃子,她舍友是个学霸叫挽鸢(超级厉害,什么都会的那种,你问她要课堂笔记没有一个是不记录的,平时都不逃课),有天燃燃子给挽鸢说,宝宝下午C语言要考试了,给我画个重点呗,考完咱俩出去吃好吃的我请客,挽鸢欣然答应,但是挽鸢当时正在学cpp的一本书,就问燃燃子能不能等她半小时,她学完就干,燃燃子一听说那好吧,你先忙,燃燃子在等待挽鸢的过程中,一会开局王者,一会刷会视频号,一会拿出书装样子看看,过了差不多半小时,燃燃子给挽鸢打电话,问她好了没,挽鸢说还有两分钟就好(怎么可能),等待是周而复始的,但燃燃子在等待挽鸢的过程中还做了其他事,所以这是非阻塞等待,打电话的过程是函数调用(调用的本质是在检测挽鸢的状态),燃燃子和挽鸢说话的过程是函数传参,挽鸢告诉燃燃子自己还需要一会的过程就是函数返回值

故事拉长,燃燃子在挽鸢的帮助下顺利考过了C语言考试,燃燃子狂喜,但是先别急着高兴,过两天考操作系统了(燃燃子:我嘞个骚刚,操作系统是啥啊),于是燃燃子顺理成章找到挽鸢,哎嘿能不能再帮我划个操作系统重点,这两天饭我包了,挽鸢说OK啊,但是挽鸢当时在学Linux网络编程,就问燃燃子能不能等她一会,她还没看完,但是燃燃子觉得来回打电话有点麻烦,就和挽鸢说你不用挂电话,就把手机放旁边,好了直接叫我就好,这个时候燃燃子墨墨听着电话那头的无尽的翻书声,只是沉默着,她什么也没干,这个时候燃燃子在进行的是阻塞等待(同时状态不就绪就不返回),这时路过一只笙宝,看燃燃子啥也不干就在那扒着手机听听听,于是笙宝过去问:“干啥呢干在这坐着”,燃燃子也不理,过会笙宝自讨没趣走了,那燃燃子为何要进行这样的苦等呢?有很多种可能,可能单纯就是想等着,还有可能是挽鸢比较受欢迎,不太容易约到(但是阻塞等待在现实中不太能存在吧,应该),waitpid检测紫禁城状态变化的

当我们采用非阻塞等待的时候,一般要加上循环,直到检测到紫禁城退出,我们把这种方案叫做非阻塞轮询方案

而阻塞等待优点也很明显了,就是简单可靠,但非阻塞时父进程可以做其他的事

各有千秋

写段非阻塞轮询的代码吧:

#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
#include<unistd.h>void ChildRun()
{int cnt = 5;while (cnt){printf("I am child process,pid:%d,ppid:%d,cnt:%d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}
}int main()
{printf("I am father process,pid:%d,pid:%d\n", getpid(), getppid());pid_t id = fork();if (id == 0){ChildRun();printf("child quit\n");exit(123);}while (1){int status = 0;pid_t rid = waitpid(id, &status, WNOHANG);	//进行非阻塞等待if (rid == 0){printf("child is running, father check next time!\n");//DoOtherThing();}else if (rid > 0){if (WIFEXITED(status)){printf("child quit success,child exit code:%d\n", WEXITSTATUS(status));}else{printf("child quit unnormal!\n");}break;}else{printf("waitpid failed!\n");break;}}return 0;
}

刚说在父进程等待的时候还可以做其他事,下面来举个栗子:基于函数指针级别的对父进程完成任务进行解耦

myprocess.c

#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
#include<unistd.h>
#include"task.h"typedef void(*func_t)();#define N 3
func_t tasks[N] = { NULL };void LoadTask()
{tasks[0] = Printlog;tasks[1] = Download;tasks[2] = MysqlDataSync;
}void HanderTask()
{for (int i = 0; i < N; i++){tasks[i]();}
}void DoOtherThing()
{HanderTask();
}void ChildRun()
{int cnt = 5;while (cnt){printf("I am child process,pid:%d,ppid:%d,cnt:%d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}
}int main()
{printf("I am father process,pid:%d,pid:%d\n", getpid(), getppid());pid_t id = fork();if (id == 0){ChildRun();printf("child quit\n");exit(123);}LoadTask();while (1){int status = 0;pid_t rid = waitpid(id, &status, WNOHANG);	//进行非阻塞等待if (rid == 0){printf("child is running, father check next time!\n");//DoOtherThing();}else if (rid > 0){if (WIFEXITED(status)){printf("child quit success,child exit code:%d\n", WEXITSTATUS(status));}else{printf("child quit unnormal!\n");}break;}else{printf("waitpid failed!\n");break;}}return 0;
}

task.h

#pragma once
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
#include<unistd.h>void Printlog();
void Download();
void MysqlDataSync();

task.c

#include"task.h"void Printlog()
{printf("begin Printlog...\n");
}void Download()
{printf("begin Download...\n");
}void MysqlDataSync()
{printf("begin MysqlDataSync...\n");
}

makefile 

myprocess:myprocess.c task.c
gcc - o $@ $ ^
.PHONT:clean
clean:rm -f myprocess

父进程就完成了在轮询检测时还做其他事 

就这些捏,到目前为止说的差不多啦,再会啦~

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 【线性代数】矩阵变换
  • 使用Top进行设备性能分析思路
  • 面试题001:Java的特点和优点,为什么要选择Java?
  • 深入Redis集群部署:从安装配置到测试验证的完整指南
  • MybatisPlus的使用与详细讲解
  • 排序算法与复杂度介绍
  • Linux的shell的date命令
  • Spring Boot 与 Amazon S3:快速上传与下载文件的完整指南
  • 从PyTorch官方的一篇教程说开去(4 - Q-table来源及解决问题实例)
  • LeetCode 125.验证回文串 C++写法
  • RDMA通信4:MR(Memory Region, 内存区域)基本概念和作用
  • html改写vue日志
  • 【银河麒麟服务器操作系统】java进程oom现象分析及处理建议
  • 计数,桶与基数排序
  • 建投数据人力资源系列产品获得欧拉操作系统及华为鲲鹏技术认证书
  • ESLint简单操作
  • Git 使用集
  • HTTP 简介
  • JavaScript的使用你知道几种?(上)
  • JavaScript设计模式系列一:工厂模式
  • miaov-React 最佳入门
  • python 学习笔记 - Queue Pipes,进程间通讯
  • React的组件模式
  • redis学习笔记(三):列表、集合、有序集合
  • 短视频宝贝=慢?阿里巴巴工程师这样秒开短视频
  • 技术胖1-4季视频复习— (看视频笔记)
  • 世界编程语言排行榜2008年06月(ActionScript 挺进20强)
  • 我看到的前端
  • 学习笔记TF060:图像语音结合,看图说话
  • 原生Ajax
  • const的用法,特别是用在函数前面与后面的区别
  • 交换综合实验一
  • #{} 和 ${}区别
  • #HarmonyOS:基础语法
  • #考研#计算机文化知识1(局域网及网络互联)
  • (6)【Python/机器学习/深度学习】Machine-Learning模型与算法应用—使用Adaboost建模及工作环境下的数据分析整理
  • (二十一)devops持续集成开发——使用jenkins的Docker Pipeline插件完成docker项目的pipeline流水线发布
  • (非本人原创)史记·柴静列传(r4笔记第65天)
  • (三)Hyperledger Fabric 1.1安装部署-chaincode测试
  • (一)Spring Cloud 直击微服务作用、架构应用、hystrix降级
  • (原創) 如何動態建立二維陣列(多維陣列)? (.NET) (C#)
  • (原創) 如何解决make kernel时『clock skew detected』的warning? (OS) (Linux)
  • (最全解法)输入一个整数,输出该数二进制表示中1的个数。
  • .NET Core实战项目之CMS 第十二章 开发篇-Dapper封装CURD及仓储代码生成器实现
  • .NET Framework与.NET Framework SDK有什么不同?
  • .NET MVC、 WebAPI、 WebService【ws】、NVVM、WCF、Remoting
  • .Net7 环境安装配置
  • .net后端程序发布到nignx上,通过nginx访问
  • .one4-V-XXXXXXXX勒索病毒数据怎么处理|数据解密恢复
  • :class的用法及应用
  • @Async注解的坑,小心
  • @Mapper作用
  • [2023-年度总结]凡是过往,皆为序章
  • [240607] Jina AI 发布多模态嵌入模型 | PHP 曝新漏洞 | TypeScript 5.5 RC 发布公告
  • [AI Google] 使用 Gemini 取得更多成就:试用 1.5 Pro 和更多智能功能