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编程入门(2)-实现who指令

Linux系统的联机帮助内容:

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

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

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

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

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

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

who指令如何工作

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

Linux编程入门(2)-实现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的工作流程可以用下图表示:

Linux编程入门(2)-实现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文件操作之写文件操作

————————————————————————————

公众号【一起学嵌入式】,一起学习、一起成长

上一篇:改丝印的假华强北三代1562A,用芯良苦!


下一篇:后端ut测试(精选)