第6章 系统数据文件和信息

第6章 系统数据文件和信息

UNIX系统的正常运作需要使用大量与系统有关的数据文件。如口令文件 /etc/passwd/和组文件/etc/group

  • 由于历史原因,这些数据文件都是 ASCII 文本文件
  • 我们可以用标准 IO 库来读取这些文件,但是也可以有专门的 API 来读取这些文件

对于数据文件的可移植界面是本章的主题。也包括了系统标识函数、时间和日期函数。

6.1 口令文件

UNIX口令文件 /etc/passwd/是一个ASCII文件,每一行包含很多字段,字段之间用冒号分隔。这些字段包含在<pwd.h>头文件定义的passwd,该结构有如下成员:

说明structpasswd成员POSIX.1
用户名char*pw_name·
加密口令char*pw_passwd
数值用户IDuid_tpw_uid·
数值组IDgit_tpw_gid·
注释字段char*pw_gecos
初始工作目录char*pw_dir·
初始shell(用户程序)char*pw_shell·

关于这些登录项请注意下列各点:

  • 通常有一个登录项,其用户名为root,其用户ID是0(超级用户)
  • 加密口令字段包含一个占位符。现在加密口令其实是放在另一个文件中
  • 口令文件中某些字段可能为空。
    • 如果加密口令字段为空,则说明该用户没有口令
    • 如果注释字段为空,则没有任何影响
  • shell字段指定了初始shell
    • 若它为空,则取系统默认值(通常是/bin/sh
    • 若它为/dev/null,则会阻止任何人以该字段所在行记录中的用户名来登录系统
  • 用户名如果是nobody,则任何人都可以使用它登录系统,但是其用户ID和组ID不提供任何特权。该用户ID和组ID只能访问人人皆可读、可写的文件
  • Linux中,没有 pw_class,pw_change,pw_expire字段

6.1.1 getpwuid、getpwnam函数

通过用户登陆名或数值用户ID就能通过两个存取口令文件函数查看相关记录。

#include <sys/types.h>
#include <pwd.h>
struct passwd* getpwuid(uid_t uid);
struct passwd* getpwnam(const char *name);
  • 参数:
    • uid:用户ID
    • name:用户名
  • 返回值:
    • 成功:返回passwd结构的指针
    • 失败:返回NULL

注意:getpwuid/getpwnam函数返回的 struct passwd结构通常是函数内部的静态变量,因此多次调用上面的函数,该静态变量会被覆写。

6.1.2 查看整个口令文件

如果只是查看一个登录名或用户ID,那么上面两个POSIX.1函数能满足要求,但是要查看整个口令文件。下列三个函数则可用于此。

#include <sys/types.h>
#include <pwd.h>
struct passwd *getpwent(void);
void setpwent(void);
void endpwent(void);
  • getpwent返回值:
    • 成功:返回passwd结构的指针
    • 失败:返回NULL
    • 到达文件尾端:返回NULL

用法:

  • 调用getpwent时,它返回口令文件中的下一个记录项

    返回的 struct passwd结构通常是函数内部的静态变量,因此多次调用getpwent函数,该静态变量会被覆写

  • 在第一次调用getpwent函数时,它会打开所使用的各个文件

  • getpwent对返回的各个记录项顺序并没有要求

  • setpwent会反绕getpwent所使用的文件到文件起始处。即当调用setpwent之后,getpwent又会从头开始读取记录项

  • endpwent会关闭getpwent所使用的文件。在调用getpwent读取完口令文件后,一定要调用endpwent关闭这些文件

    getpwent知道什么时候应该打开它所使用的文件(第一次被调用时),但是不知道何时应该关闭这些文件。

案例:getpwnam的一个实现

#include <sys/types.h>
#include <pwd.h>
#include <stddef.h>
#include <string.h>
struct passwd* getpwnam(const char* name){
    struct passwd *ptr;
    if(setpwent() != 1){
        // 打开 /etc/passwd 文件失败
        return NULL;
    }    
    while((ptr = getpwent()) != NULL) {
        if(strcmp(name, ptr->pw_name) == 0)
            endpwent(); //关闭 /etc/passwd 文件
            return ptr;
    }
    endpwent();// 关闭 /etc/passwd 文件
    return(ptr);// 用户名未找到
}

程序在开始处调用setpwent是保护性措施,以便在调用者在此之前已经调用过getpwent的情况下,反绕有关文件使它们定位到文件开始处。

6.2 阴影口令

加密口令是经单向加密算法处理过的用户口令副本。

因为此算法是单向的,所以不能从加密口令猜测到原始口令

现在的UNIX将加密口令存放在一个称作阴影口令的文件中(即文件/etc/shadow)。该文件至少应该包含用户名和加密口令。

这些字段包含在<shadow.h>头文件的struct spwd结构中。相关的字段如下:

说明shadow成员
用户登陆名char* sp_namp
加密口令char* sp_pwdp
上次更改口令以来经过的时间int sp_lstchg
经过多少天后允许修改口令int sp_min
经过多少天后必须修改口令int sp_max
经过多少天后如果未修改口令则系统发出警告int sp_warn
经过多少天后,该账户是inactiveint sp_inact
经过多少天后,该账户过期int sp_expire
保留字段unsigned int sp_flag

其中只有用户登录名和加密口令这两个字段是必须的。其他字段都是用于控制口令更改的频率。

注意:

  • 阴影口令文件/etc/shadow 不应该由一般用户读取。
    • 仅有少数几个程序需要访问加密口令,如login,passwd。这些程序通常是设置用户ID为root的程序
    • 普通口令文件/etc/passwd/可以任由各用户读取

6.3 组文件

UNIX 组文件包含的字段定义在<grp.h>所定义的group结构中:

说明struct group成员POSIX.1
组名char* gr_name·
加密口令char* gr_passwd
数字组IDint gr_gid·
指向各用户名指针的数组char **gr_men·

字段gr_men是一个指针数组,其中的指针个指向一个属于该组的用户名。该数组以NULL结尾。

6.2.1 getgrgid、getgrnam函数

查看组名或数值组ID。

#include <sys/types.h>
#include <grp.h>
struct group* getgrgid(git_t gid);
struct group* getgrnam(const char* name);
  • 参数:
    • gid:组ID
    • name:组名
  • 返回值:
    • 成功:返回group结构的指针
    • 失败:返回NULL

注意:getgrgid/getgrnam函数返回的 struct group结构通常是函数内部的静态变量,因此多次调用上面的函数,该静态变量会被覆写。

6.2.2 查看整个组文件

#include <grp.h>
struct group* getgrent(void);
void setgrent(void);
void endgrent(void);
  • getgrent返回值:
    • 成功:返回group结构的指针
    • 失败:返回NULL
    • 到达文件尾端:返回NULL

用法:

  • 调用getgrent时,它返回组文件中的下一个记录项

    返回的 struct group结构通常是函数内部的静态变量,因此多次调用getgrent函数,该静态变量会被覆写

  • 在第一次调用getgrent函数时,它会打开所使用的各个文件

  • getgrent对返回的各个记录项顺序并没有要求

  • setgrent会反绕getgrent所使用的文件到文件起始处。即当调用setgrent之后,getgrent又会从头开始读取记录项

  • endgrent会关闭getgrent所使用的文件。在调用getgrent读取完组文件后,一定要调用endgrent关闭这些文件

    getgrent知道什么时候应该打开它所使用的文件(第一次被调用时),但是不知道何时应该关闭这些文件

6.4 添加组ID

优点:不必在显示地经常更改组

存取和设置添加组ID:

#include <sys/types.h>
#include <unistd.h>
int getgroups(int gidsetsize, gid_t grouplist[]);
int setgroups(int ngroups, const gid_t grouplist[]);
int initgroups(const char* username, gid_t basegid);

参数:

  • 对于getgroups函数:

    • gidsetsize:填入grouplist数组的附属组ID的最大数量

      若该值为0,则函数只返回附属组ID数,而不修改grouplist数组

    • grouplist:存放附属组ID的数组

  • 对于setgroups函数:

    • ngroupsgrouplist数组中元素个数

      数量不能太大,不能超过NGROUPS_MAX

    • grouplist:待设置的附属组ID的数组

  • 对于initgroups函数:

    • username:用户名
    • basegid:用户的base组ID(它就是在口令文件中,用户名对于的组ID)

返回值:

  • 对于getgroups函数:
    • 成功:返回附属组ID的数量
    • 失败:返回 -1
  • 对于setgroups/initgroups函数:
    • 成功:返回 0
    • 失败:返回 -1

用法:

  • getgroups函数将进程所属用户的各附属组ID填写到grouplist中,填入该数组的附属组ID数最多为gidsetsize个。实际填写到数组中的附属组ID数由函数返回
  • setgroups函数可由超级用户调用以便为调用进程设置附属组ID表。
  • 由于initgroups函数会在内部调用setgroups函数,因此它也必须由超级用户调用

6.5 其他数据文件

除了口令文件和组文件之外,系统中还有很多其他重要的数据文件。UNIX对于这些系统数据文件提供了对应的类似的API。对于每种数据文件,至少有三个函数:

  • get函数:读下一个记录。如果需要还会打开该文件。
    • 此种函数通常返回一个指向某个结构的指针。
    • 当已到达文件尾端时,返回空指针
    • 大多数get函数返回指向一个静态存储类结构的指针,如果需要保存其内容,则需要复制该结构
  • set函数:打开相应数据文件(如果尚未打开),然后反绕该文件
    • 如果希望在相应文件起始处开始处理,则调用该函数
  • end函数:关闭相应数据文件。在结束了对相应数据文件的读、写操作后,总应该调用此函数以关闭所有相关文件

另外如果数据文件支持某种形式的键搜索,则也提供搜索具有指定键的记录的函数

下面是各个重要的数据文件:

说明数据文件头文件结构附加的键搜索函数
口令/etc/passwd<pwd.h>passwdgetpwnam,getpwuid
/etc/group<grp.h>groupgetgrnam,getgrgid
阴影/etc/shadow<shadow.h>spwdgetspnam
主机/etc/hosts<netdb.h>hostentgetnameinfo,getaddrinfo
网络/etc/networks<netdb.h>netentgetnetbyname,getnetbyaddr
协议/etc/protocols<netdb.h>protoentgetprotobyname,getprotobynumber
服务/etc/services<netdb.h>serventgetservbyname,getservbyport

6.6 登陆会计

大多数UNIX系统都提供了两个数据文件:

  • utmp文件:记录了当前登录到系统的各个用户
  • wtmp文件:跟踪各个登录和注销事件

每次写入到这两个文件的是下列结构的一个二进制记录:

struct utmp{
    char ut_line[8];//登录地tty
    char ut_name[9];//登陆用户名
    long ut_time;//自1970.01.01 00:00:00 经过的秒数
}
  • 登录时,login程序填写此类结构,然后将其写入到utmp文件中,同时也将其添写到wtmp文件中

  • 注销时,init进程将utmp文件中相应的记录擦除(每个字节都填写null字节),并将一个新的记录添写到wtmp文件中

  • 在系统重启时,以及更改系统时间和日期的前后,都将在wtmp文件中追加写特殊的记录项

    who程序会读取utmp文件;last程序会读取wtmp文件

linux系统中,这两个文件的路径是/var/run/utmp以及/var/log/wtmp

6.7 系统标识

6.7.1 uname函数

查看主机和操作系统有关的信息:

#include <sys/utsname.h>
int uname(struct utsname *name);
  • 参数:
    • name:一个utsname结构的地址,该函数会填写此结构
  • 返回值:
    • 成功: 返回非负值
    • 失败: 返回 -1

POSIX 定义了utsname结构最少需要提供的字段(全部是字符数组),某些操作系统会在该结构中提供了另外一些字段:

struct utsname {
	char sysname[];  //操作系统的名字
	char nodename[]; // 节点名字
	char release[];  //当前操作系统的 release
	char version[];  //该 release 的版本
	char machine[];  //硬件类型
}

这些字符串都是以null结尾。

通常 uname 命令会打印utsname结构中的信息

6.7.2 gethostname函数

返回主机名。改名字通常就是 TCP/IP 网络上主机的名字:

#include<unistd.h>
int gethostname(char* name, int namelen);
  • 参数:

    • name:放置主机名字符串的缓冲区

    • namelenname缓冲区的长度

      如果缓冲区够长,则通过name返回的字符串以null结尾;如果缓冲区不够长,则标准没有说通过name返回的字符串是否以null结尾

  • 返回值:

    • 成功: 返回 0
    • 失败: 返回 -1

hostname命令可以获取和设置主机名

6.8 时间和日期例程

UNIX在这方面与其他操作系统的区别是:

  • 以国际标准时间而非本地时间计时;
  • 可自动进行转换,例如变换到夏日制;
  • 将时间和日期作为一个量值保存。time函数返回当前时间和日期。
#include <time.h>
time_t time(time_t *calptr);
  • 参数:
    • calptr:如果该指针不是NULL,则返回的日历时间也存放在calptr指向的内存中
  • 返回值:
    • 成功:返回当前日历时间的值
    • 失败:返回 -1
  1. gmtime/localtime函数:将日历时间转换成struct tm结构:

    #include<time.h>
    struct tm* gmtime(const time_t *calptr);
    struct tm* localtime(const time_t *calptr);
    
    • 参数:calptr:指向日历时间的指针
    • 返回值:
      • 成功:指向struct tm结构的指针
      • 失败:返回NULL
    struct tm{
    	int tm_sec; 	//秒数,范围是 [0~60]
    	int tm_min; 	//分钟数,范围是 [0~59]
    	int tm_hour;	//小时数,范围是 [0~23]。午夜12点是 0
    	int tm_mday;	//一个月中的天数,范围是 [1~31]
    	int tm_mon; 	//月数,范围是 [0~11] ,一月是 0	
    	int tm_year;	//年数,范围是 [1900~],如果是16则表示 1916	
    	int tm_wday;	//一个星期中的天数,范围是 [0~6] ,周日是0
    	int tm_yday;	//一年中的天数,范围是 [0~365],一月一号是 0
    	int tm_isdst;  //daylight saving time flag
    }
    

    其中秒可以超过 59 的理由是表示润秒

    gmtime/localtime函数的区别:

    • gmtime:将日历时间转换成统一协调的年月日时分秒周日分解结构
    • localtime:将日历时间转换成本地实际(考虑本地市区和夏令时标志),由TZ环境变量指定

    TZ环境变量影响localtime/mktime/strftime这三个函数:

    • 如果定义了TZ环境变量:则这些函数将使用TZ的值代替系统默认时区
    • 如果TZ定位为空TZ=,则使用UTC作为时区
  2. mktime函数:以本地时间的年月日等作为参数,将其变化成time_t值:

    #include<time.h>
    time_t mktime(struct tm*tmptr);
    
    • 参数: tmptr:指向struct tm结构的指针
    • 返回值:
      • 成功: 返回日历时间
      • 失败: 返回 -1

    所谓的本地实际的”本地“:由 TZ环境变量指定

  3. strftime/strftime_l函数:类似printf的打印时间的函数。它们可以通过可用的多个参数来定制产生的字符串

    #include<time.h>
    size_t strftime(char *restrict buf,size_t maxsize,const char*restrict format,
    	const struct tm* restrict tmptr);
    size_t strftime_l(char *restrict buf,size_t maxsize,const char*restrict format,
    	const struct tm* restrict tmptr,locale_t locale);
    
    • 参数:

      • buf:存放格式化后的时间字符串的缓冲区的地址
      • maxsize:存放格式化后的时间字符串的缓冲区的大小
      • format:时间的格式化字符串
      • tmptr:存放时间的struct tm结构的指针

      对于strftime_l 函数:

      • locale:指定的区域
    • 返回值:

      • 成功:返回存入buf的字符数
      • 失败: 返回 0

    注意:

    • 如果buf长度足够存放格式化结果以及一个null终止符,则这两个函数才有可能顺利转换;否则空间不够,这两个函数返回0,表示转换失败
    • strftime_l运行调用者将区域指定为参数;而strftime使用通过TZ环境变量指定的区域
    • format参数控制时间值的格式。如同printf,转换说明的形式是百分号之后跟随一个特定的字符,而format中的其他字符则按照原样输出:
      • %a:缩写的周日名,如Thu
      • %A:周日名,如Thursday
      • %b:缩写的月名:如Jan
      • %B:全月名,如January
      • %c:日期和时间,如Thu Jan 19 21:24:25 2012
      • %C:年的最后两位,范围是(00~99),如20
      • %d:月日,范围是 (01~31),如19
      • %D日期(MM/DD/YY),如01/19/12
      • %e月日(一位数字前加空格)(1~31),如19
      • %F:ISO 8601 日期格式 (YYYY-MM-DD),如 2012-01-19
      • %g:ISO 8601 年的最后2位数(00~99),如12
      • %G:ISO 8601 的年,如 2012
      • %h:与 %b 相同,缩写的月名
      • %H:小时(24小时制)(00~23)
      • %I:小时(12小时制)(01~12)
      • %j:年日(001~366),如019
      • %m:月(01~12),如 01
      • %M:分(00~59),如 24
      • %n:换行符
      • %pAM/PM
      • %r:本地时间(12小时制),如 09:24:52 PM
      • %R:与 %H:%M相同
      • %S:秒(00~60),如 52
      • %t:水平制表符
      • %T:同 %H:%M:%S 相同,如 21:24:52
      • %u:ISO 8601 周几(1~7,1为周一)
      • %U:一年的星期日周数(00~53)
      • %V:ISO 8601 周数(01~53)
      • %w:周几:(0~6,周日为0)
      • %W:一年的星期一周数(00~53)
      • %x:本地日期,如 01/19/12
      • %X:本地时间,如21:24:52
      • %y:年的最后两位(00~99)
      • %Y:年,如2012
      • %z:ISO 8601 格式的UTC偏移量,如 -0500
      • %Z:时区名,如EST
      • %%:百分号
  4. strptime函数:它是strftime的逆向过程,把时间字符串转换成struct tm时间

    #include<time.h>
    char *strptime(const char*restrict buf,const char*restrict format,
    	struct tm*restrict tmptr);
    
    • 参数:
      • buf:存放已经格式化的时间字符串的缓冲区的地址
      • format:给出了buf缓冲区中的格式化时间字符串的格式
      • tmptr:存放时间的struct tm结构的指针
    • 返回值:
      • 成功:返回非NULL
      • 失败:返回NULL

    注意:strptime的格式化说明与strftime的几乎相同,但是下列会有区别

    • %a:缩写或者完整的周日名
    • %A:同%a
    • %b:缩写或者完整的月名
    • %B:同%b
    • %n:任何空白
    • %t:任何空白

本章小结

在所有UNIX系统上都使用口令文件和组文件。我们说明了各种读这些文件的函数。也介绍了阴影口令,它可以增加系统的安全性。添加组ID正在得到更多应用,它提供了一个用户同时可以参加多个组的方法。还介绍了大多数系统所提供的存取其他与系统有关的数据文件的类似的函数。最后,说明了ANSIC和POSIX.1提供的与时间和日期有关的一些函数。

习题

6.1 如果系统使用阴影文件,如何取得加密口令?

普通用户和普通程序无法直接获取加密口令,获取加密口令通常需要特权和系统管理员的权限,并且必须遵循系统的安全策略。

阴影口令(shadow password)是一种更加安全的方式来存储用户密码,通常将加密密码存储在 /etc/shadow 文件中,而不是 /etc/passwd 文件中。这种安全性是通过限制对阴影文件的访问来实现的,通常只有系统管理员或具有特权的用户可以访问该文件。

在Unix/Linux系统中,一般的密码验证方法是通过标准的身份验证工具来验证用户身份,而不是获取用户的明文密码。因此,阴影文件的设计目的之一是为了提高密码的安全性,阻止非授权的访问和泄漏。

6.2 假设你有超级用户许可权,并且系统使用了阴影口令,重新考虑上一道习题。

在Unix/Linux系统中,阴影文件通常是 /etc/shadow,存储了用户的加密密码。只有root用户或具有sudo权限的用户才能访问该文件。因此,作为root用户,你可以通过以下方法来获取加密口令:

  1. 直接读取 /etc/shadow 文件:使用文本编辑器或命令行工具,你可以打开 /etc/shadow 文件并查看其中的内容。这将包括用户的加密密码,通常是一个经过哈希加密的字符串。

  2. 使用专门的密码破解工具:虽然直接读取 /etc/shadow 文件是一种方式,但密码通常是经过哈希处理的,所以不容易直接解密。但你可以使用专门的密码破解工具,如John the Ripper,来尝试破解加密密码。这通常需要一些计算时间,特别是对于强密码。

代码编译后,使用root权限运行。

#include <iostream>
#include <string>
#include <shadow.h>

int main(int argc, char* argv[]){
    std::string name;
    std::cout << "input your user name: ";
    std::cin >> name;
    struct spwd* ptr = nullptr;
    ptr = getspnam(name.c_str());
    if(ptr){
        std::cout << ptr->sp_pwdp << std::endl;
    }
}

这个程序会提示用户输入用户名,然后使用 getspnam 函数来获取阴影文件中的密码信息,最后将密码信息输出到标准输出。

6.3 编程调用uname并输出utsname结构中的所有字段,并与uname(1)命令的输出结果作比较。

使用uname函数获取系统信息,并输出utsname结构中的所有字段。

#include <stdio.h>
#include <sys/utsname.h>
int main(){
    struct utsname uts;
    if(uname(&uts) == -1){
        perror("uname");
        return 1;
    }
    printf("System Name: %s\n",uts.sysname);
    printf("Node name: %s\n",uts.nodename);
    printf("Release: %s\n",uts.release);
    printf("Version: %s\n",uts.version);
    printf("Machine: %s\n",uts.machine);
    return 0;
}

此程序包含以下步骤:

  1. 包含必要的头文件,包括 <stdio.h><sys/utsname.h>
  2. 声明一个 struct utsname 类型的变量 uts 用于存储系统信息。
  3. 使用 uname(&uts) 函数来获取系统信息并存储在 uts 结构中。
  4. 使用 printf 函数输出 utsname 结构中的各个字段,例如系统名称、节点名称、版本等。
  5. 运行程序并比较输出结果与 uname 命令的输出结果。

与uname(1)命令的输出结果相比少三个输出:处理器类型、硬件平台、操作系统。因为UNIX系统并不负责定义硬件及具体实现,UNIX只定义标准规范。

6.4 编程获取当前时间并使用strftime将输出结果转换为类似于date(1)命令的缺省输出。修改环境变量TZ的值,观察输出的结果。

#include <iostream>
#include <climits>
#include <ctime>

int main(int argc, char *argv[]) {
    // 获取当前时间的时间戳
    time_t tm_t = time(nullptr);
    // 用于存储本地时间的结构
    tm* tmp = nullptr;
    // 用于存储格式化后的时间字符串
    char buf[64] = {0};
    // 将时间戳转换为本地时间结构
    if (tmp = localtime(&tm_t)) {
        // 格式化时间字符串
        strftime(buf, 63, "%Y年 %m月 %e日 星期%u  %H:%M:%S  %Z\n", tmp);
    }
    // 输出格式化后的时间字符串
    std::cout << "\n个人程序:";
    std::cout << "\n" << buf << std::endl;
    return 0;
}

这些注释可以帮助理解代码的各个部分以及其功能。这段代码的主要目的是获取当前时间并将其格式化为包含年份、月份、日期、星期、小时、分钟、秒和时区的字符串,并将其输出到标准输出流。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
《现代操作系统》第四版第六主要讨论了操作系统的内存管理和存储管理的相关内容。在这一中,我们学习了操作系统如何管理计算机的内存资源,包括内存分配、虚拟内存和内存映射等技术。此外,我们也了解了存储管理的相关概念,包括文件系统、存储设备和磁盘管理等内容。 首先,我们学习了内存管理的重要性,以及操作系统是如何管理和分配内存资源的。内存分配是指操作系统如何将计算机的内存空间分配给不同的程序或进程,以便它们能够正常运行。虚拟内存则是指操作系统如何利用磁盘空间来扩展内存,以便能够运行更多的程序或进程。内存映射则是指操作系统如何将虚拟内存映射到物理内存,以便程序能够访问和操作内存资源。 其次,我们学习了存储管理的相关内容,包括文件系统的组织和管理、存储设备的管理以及磁盘管理的技术。文件系统是操作系统用来组织和管理存储设备上的文件和目录的一种技术,它能够帮助用户方便地存储和访问文件。存储设备管理则是指操作系统如何管理计算机上的存储设备,包括硬盘、固态硬盘和光盘等设备。磁盘管理则是指操作系统如何管理磁盘上的存储空间,以便能够高效地存储和访问数据。 综上所述,《现代操作系统》第四版第六涵盖了操作系统的内存管理和存储管理的相关内容,帮助我们更好地理解和掌握操作系统的相关技术和原理。这些知识不仅对我们理解计算机系统和性能优化有重要意义,也能够帮助我们在实际工作中更好地使用和管理计算机系统

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

霜晨月c

谢谢老板地打赏~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值