Linux编程入门(2)-实现who指令

上一篇简单介绍了Linux系统编程的一些概念知识,从本篇文章开始,从解释系统命令的功能入手,由浅入深,逐步讲解Linux系统编程。

建议学习者最好具有一定的C语言基础,了解数组、结构体、指针和链表的概念。

代码实验环境

操作系统:Ubuntu 18.04 LTS

编译器gcc版本:gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0

学习目标

通过分析who指令,来学习Linux的读文件操作。

who指令介绍

Linux为多用户操作系统,有时候需要查看系统是否繁忙,某人是否正在使用系统等,可以使用who指令来查看Linux系统中活动用户的情况。

命令也是程序。Linux系统中,几乎所有的命令都是人为编写的程序。在Linux系统的中增加新的命令很简单,把可执行文件放到以下任意一个目录即可:/bin、/usr/bin、/usr/local/bin,这些目录存放着很多系统命令。

如果想知道谁在使用系统,输入who指令,输出如下:

$ who

user :0 2021-10-31 21:42 (:0)

test pts/1 2021-10-31 23:19 (192.168.0.104)

每一行代表一个已经登陆的用户,第一列是用户名,第二列是终端名,第三列是登陆时间,第四列是用户的登陆地址。

who指令详解

我们可以通过联机帮助指令man,来查看who的使用方法和详细解释。查看who的帮助可输入:

$ man who

Linux系统的联机帮助内容:

名字(NAME):命令的名字以及对这个命令的简短说明。

概要(SYNOPSIS):给出命令的用法说明,包括命令格式、参数和选项列表。方括号([OPTION])为可选项。选项为短线 - 加上abdHlmpqrstTu这些字母的任意组合,命令末尾还可以有一个文件参数或者给定两个参数。

描述(DESCRIPTION):关于指令的详细阐述。根据指令和平台的不同,描述的内容也不同。

选项(OPTIONS):给出命令行中每一个选项的说明。

作者(AUTHOR):命令的作者。

参阅(SEE ALSO):包含这个命令相关的其他主题。

who指令如何工作

向下翻阅 man who指令看到的帮助信息,有以下信息

圈出的内容说明,如果who命令没有指定文件,通常用 /var/run/utmp/var/log/wtmp作为选项文件。

/var/run/utmp 文件保存当前登陆系统的用户信息

/var/log/utmp 文件保存登陆过本系统的用户信息

who通过读取文件/var/run/utmp 获得当前系统登陆的用户信息。

utmp这个文件里保存的是结构体数组,数组元素是utmp类型的结构,可以utmp.h中找到utmp类型的定义。文件utmp.h存放在/usr/include目录下。

文件/usr/include/utmp.h部分内容如下(已删除无关代码):

#ifndef _UTMP_H
#define _UTMP_H 1
#include <features.h>
#include <sys/types.h>
__BEGIN_DECLS
/* Get system dependent values and data structures. */
#include <bits/utmp.h>
/* Compatibility names for the strings of the canonical file names. */
#define UTMP_FILE _PATH_UTMP
#define UTMP_FILENAME _PATH_UTMP
#define WTMP_FILE _PATH_WTMP
#define WTMP_FILENAME _PATH_WTMP
#endif /* Use misc. */
__END_DECLS
#endif /* utmp.h */

utmp的具体结构定义在 bits/utmp.h文件中。如下:

#define EMPTY 0
#define BOOT_TIME 2
#define NEW_TIME 3
#define OLD_TIME 4
#define INIT_PROCESS 5
#define LOGIN_PROCESS 6
#define USER_PROCESS 7
#define DEAD_PROCESS 8
#define ACCOUNTING 9
#define UT_LINESIZE 32
#define UT_NAMESIZE 32
#define UT_HOSTSIZE 256
struct exit_status {
short int e_termination;
short int e_exit;
};
struct utmp {
short ut_type;
pid_t ut_pid;
char ut_line[UT_LINESIZE];
char ut_id[4];
char ut_user[UT_NAMESIZE];
struct exit_status ut_exit;
#if __WORDSIZE == 64 && defined __WORDSIZE_COMPAT32
int32_t ut_session;
struct {
int32_t tv_sec;
int32_t tv_usec;
} ut_tv;
#else
long ut_session;
struct timeval ut_tv;
#endif
int32_t ut_addr_v6[4];
char __unused[20];
};
/* 向后兼容定义 */
#define ut_name ut_user
#ifndef _NO_UT_TIME
#define ut_time ut_tv.tv_sec
#endif
#define ut_xtime ut_tv.tv_sec
#define ut_addr ut_addr_v6[0]

由以上分析可知,who通过读文件来获取需要的信息,而每个登陆的用户在文件中都有对应的记录。who的工作流程可以用下图表示:

/var/run/utmp文件中的结构数组存放已登陆用户的信息,who指令的实现,是不是把记录一个一个地读出并显示出来呢?让我们继续分析。

实现who命令

编写who程序时,需要做两件事:

  • 从文件(/var/run/utmp)中读取数据结构信息
  • 以合适的形式将结构中的信息显示出来
第一步:读取信息

从某个文件中读取数据,Linux系统提供了三个系统函数:open()、read()、close()。

  • open() —— 打开一个文件

open在Linux下的定义以及调用函数所需的头文件如下:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

函数第一个参数pathname,是要打开的文件的路径名或者文件名。

第二个参数flags,表示打开文件的操作模式(有3种):只读(O_RDONLY)、只写(O_WRONLY)、可读可写(O_RDWR),调用此函数时,必须指定其中一种。还有其他可选模式,暂不做介绍。

第三个参数mode,表示设置文件访问权限的初始值,和用户掩码umask有关。此文暂时不用这个参数。

打开文件时,如果操作成功,内核会返回一个正整数的值,这个数值叫做文件描述符。如果内核检测到任务错误,这个系统调用会返回-1。

要对一个文件进行操作(读或者写),必须先打开文件。文件打开成功后,可以通过文件描述符对文件进行操作。

  • read() —— 从文件读取数据

read在Linux下的定义以及调用函数所需的头文件如下:

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);

函数第一参数fd,为文件描述符,由open函数返回。

第二个参数buf,存放读取数据的内存空间。

第三个参数count,希望读取的数据的个数。

如果读取成功,返回所读取数据的字节个数;否则,返回-1。

注意:最终读取的数据可能没有要求的多。例如,文件中剩余的数据少于要求读取的个数,则程序只能读取文件中剩余的数据个数。当读到文件末尾时,函数会返回0。

  • close() —— 关闭文件

clsoe在Linux下的定义以及调用函数所需的头文件如下:

#include <unistd.h>
int close(int fd);

close 这个系统函数会关闭已经打开的文件,fd为open()函数打开文件返回的描述符。如果关闭出错,函数返回-1。关闭成功,则返回0。

对文件的操作完成后,需要关闭文件,以减少内存资源占用。

第二步:显示信息

通过printf函数利用定宽度的格式显示utmp记录信息。

第三步:代码实现

初步代码实现如下:

#include<stdio.h>
#include<utmp.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdlib.h>
#define SHOWHOST
void show_info(struct utmp *utbufp);
int main()
{
struct utmp current_record;
int utmpfd;
int reclen = sizeof(current_record);
if((utmpfd = open(UTMP_FILE, O_RDONLY)) == -1)
{
perror(UTMP_FILE);
exit(1);
}
while(read(utmpfd, &current_record, reclen) == reclen)
{
show_info(&current_record);
}
close(utmpfd);
return 0;
}
// 显示信息
void show_info(struct utmp *utbufp)
{
printf("%-8.8s", utbufp->ut_name);
printf(" ");
printf("%-8.8s", utbufp->ut_line);
printf("%10d", utbufp->ut_time);
printf(" ");
#ifdef SHOWHOST
printf("(%s)", utbufp->ut_host);
#endif
printf("\n");
}

编译

$gcc who1.c -o who1

运行结果如下

$./who1
reboot ~ 1635728912 (4.15.0-161-generic)

runlevel ~ 1635729058 (4.15.0-161-generic)

user :0 1635729148 (:0)

test pts/2 1635763291 (192.168.0.104)

将上述输出结果与系统who命令输出做对比:

$ who

user :0 2021-11-01 09:12 (:0)

test pts/2 2021-11-01 18:41 (192.168.0.104)

自己编写的who已经可以工作了,可以显示用户名、终端名、远程主机名。但是,根系统的who相比较还不完善。存在两处内容需要改进:

(1)消除空白记录

(2)正确显示登陆时间

程序代码优化

  • 消除空白记录

系统who命令只列出已登陆用户的信息。而我们编写的代码,除了列出已登录的用户,还会显示utmp文件中的其他信息。实际上utmp包含所有终端的信息,那些尚未用到的终端信息也会存放在utmp中。

utmp结构中有一个成员ut_type,当它的值为7(USER_PROCESS)时,表示这是一个已经登陆的用户。据此,对原来的程序显示信息函数 show_info() 函数开头添用户类型判断,即可消除空白记录:

if(utbufp->ut_type != USER_PROCESS)
{
return;
}
  • 使得显示登陆时间可读

Linux中的时间是用一个整数来表示的,类型为 time_t ,它的数值是从1970年1月1日0时开始经过的秒数。存储时间的结构 time_t 实际上就是 long int 。类型 time_t 定义为

typedef long int time_t;

需要将时间的整数值转换为易于理解的形式。实验环境系统中who指令显示的时间格式如下

2021-10-31 23:19

我们需要由存储的时间的秒数值得到:年、月、日、时、分等信息。即需要将Linux存储的时间秒数转换为分解时间。分解时间存储结构类型为tm,其结构定义如下

struct tm
{
int tm_sec; /* 秒 (0-59) */
int tm_min; /* 分 (0-59) */
int tm_hour; /* 小时 (0-23) */
int tm_mday; /* 一个月中第几天 (1-31) */
int tm_mon; /* 月份 (0-11) */
int tm_year; /* 自 1900 年起的年数 */ int tm_wday; /* 一周中第几天 (0-6, Sunday = 0) */
int tm_yday; /* 一年中第几天 (0-365, 1 Jan = 0) */
int tm_isdst; /* 夏令时 */
};

localtime()函数将时间秒数转换为分解时间,并用本地时区表示,其定义如下

#include <time.h>
struct tm *localtime(const time_t *timep);

函数的参数为一个指向 time_t 的指针,返回一个指向 tm 结构的指针。

  • 代码优化

综合以上两点对代码进行优化。

优化信息显示函数show_info如下:

void show_info(struct utmp *utbufp)
{
if(utbufp->ut_type != USER_PROCESS)
{
return;
}
printf("%-8.8s", utbufp->ut_name);
printf(" ");
printf("%-8.8s", utbufp->ut_line);
show_time(utbufp->ut_time); printf(" ");
#ifdef SHOWHOST
printf("(%s)", utbufp->ut_host);
#endif
printf("\n");
}

添加时间显示函数show_time如下

void show_time(time_t timeval)
{
struct tm *info = NULL;
info = localtime(&timeval);
printf("%4d-%2d-%02d %02d:%02d", (info->tm_year + 1900), (info->tm_mon + 1), \
info->tm_mday, info->tm_hour, info->tm_min);
}

编译后,运行结果如下

$./who2

user :0 2021-11-01 09:12 (:0)

user pts/2 2021-11-01 18:41 (192.168.0.104)

显示的结果与系统的who命令对比,显示结果基本一致。

小结

本篇文章介绍了Linux系统中who命令的工作原理,并通过自己实现who指令,来学习Linux编程对文件的读操作。并学习了登陆信息utmp文件结构,学习了Linux的时间处理。

涉及到的系统函数:open、read、close、localtime

相关指令:man、who

后续

接下来学习Linux文件操作之写文件操作


关注微信公众号【一起学嵌入式】,获取更多精彩内容

posted @   一起学嵌入式  阅读(609)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示