分类 技术 下的文章

Rust FFI 和 bindgen 工具是为 Rust 调用 C 库而设计的。Rust 很容易与 C 语言对话,从而与任何其它可以与 C 语言对话的语言对话。

为什么要从 Rust 调用 C 函数?简短的答案就是软件库。冗长的答案则触及到 C 在众多编程语言中的地位,特别是相对 Rust 而言。C、C++,还有 Rust 都是系统语言,这意味着程序员可以访问机器层面的数据类型与操作。在这三个系统语言中,C 依然占据主导地位。现代操作系统的内核主要是用 C 来写的,其余部分依靠汇编语言补充。在标准系统函数库中,输入与输出、数字处理、加密计算、安全、网络、国际化、字符串处理、内存管理等等,大多都是用 C 来写的。这些函数库所代表的是一个庞大的基础设施,支撑着用其他语言写出来的应用。Rust 发展至今也有着可观的函数库,但是 C 的函数库 —— 自 1970 年代就已存在,迄今还在蓬勃发展 —— 是一种无法被忽视的资源。最后一点是,C 依然还是编程语言中的 通用语:大部分语言都可以与 C 交流,透过 C,语言之间可以互相交流。

两个概念证明的例子

Rust 支持 FFI( 外部函数接口 Foreign Function Interface )用以调用 C 函数。任何 FFI 所需要面临的问题是调用方语言是否涵盖了被调用语言的数据类型。例如,ctypes 是 Python 调用 C 的 FFI,但是 Python 并没有包括 C 所支持的无符号整数类型。结果就是,ctypes 必须寻求解决方案。

相比之下,Rust 包含了所有 C 中的原始(即,机器层面)类型。比如说,Rust 中的 i32 类对应 C 中的 int 类。C 特别声明了 char 类必须是一个字节大小,而其他类型,比如 int,必须至少是这个大小(LCTT 译注:原文处有评论指出 int 大小依照 C 标准应至少为 2 字节);然而如今所有合理的 C 编译器都支持四字节的 int,以及八字节的 double(Rust 中则是 f64 类),以此类推。

针对 C 的 FFI 所面临的另一个挑战是:FFI 是否能够处理 C 的裸指针,包括指向被看作是字符串的数组指针。C 没有字符串类型,它通过结合字符组和一个非打印终止符(大名鼎鼎的 空终止符)来实现字符串。相比之下,Rust 有两个字符串类型:String&str (字符串切片)。问题是,Rust FFI 是否能将 C 字符串转化成 Rust 字符串——答案是 肯定的

出于对效率的追求,结构体指针在 C 中也很常见。一个 C 结构体在作为一个函数的参数或者返回值的时候,其默认行为是传递值(即,逐字节复制)。C 结构体,如同它在 Rust 中的对应部分一样,可以包含数组和嵌套其他结构体,所以其大小是不定的。结构体在两种语言中的最佳用法是传递或返回引用,也就是说,传递或返回结构体的地址而不是结构体本身的副本。Rust FFI 再一次成功处理了 C 的结构体指针,其在 C 函数库中十分普遍。

第一段代码案例专注于调用相对简单的 C 库函数,比如 abs(绝对值)和 sqrt(平方根)。这些函数使用非指针标量参数并返回一个非指针标量值。第二段代码案例则涉及了字符串和结构体指针,在这里会介绍工具 bindgen,其通过 C 接口(头文件)生成 Rust 代码,比如 math.h 以及 time.h。C 头文件声明了 C 函数的调用语法,并定义了会被调用的结构体。两段代码都能在 我的主页上 找到。

调用相对简单的 C 函数

第一段代码案例有四处 Rust 对标准数学库内的 C 函数的调用:两处分别调用了 abs(绝对值)和 pow(幂),两处重复调用了 sqrt(平方根)。这个程序可以直接用 rustc 编译器进行构建,或者使用更方便的命令 cargo build

use std::os::raw::c_int;  // 32位
use std::os::raw::c_double; // 64位

// 从标准库 libc 中引入三个函数。
// 此处是 Rust 对三个 C 函数的声明:
extern "C" {
  fn abs(num: c_int) -> c_int;
  fn sqrt(num: c_double) -> c_double;
  fn pow(num: c_double, power: c_double) -> c_double;
}

fn main() {
  let x: i32 = -123;
  println!("\n{x}的绝对值是: {}.", unsafe { abs(x) });

  let n: f64 = 9.0;
  let p: f64 = 3.0;
  println!("\n{n}的{p}次方是: {}.", unsafe { pow(n, p) });

  let mut y: f64 = 64.0;
  println!("\n{y}的平方根是: {}.", unsafe { sqrt(y) });

  y = -3.14;
  println!("\n{y}的平方根是: {}.", unsafe { sqrt(y) }); //** NaN = NotaNumber(不是数字)
}

顶部的两个 use 声明是 Rust 的数据类型 c_intc_double,对应 C 类型里的 intdouble。Rust 标准模块 std::os::raw 定义了 14 个类似的类型以确保跟 C 的兼容性。模块 std::ffi 中有 14 个同样的类型定义,以及对字符串的支持。

位于 main 函数上的 extern "C" 区域声明了 3 个 C 库函数,这些函数会在 main 函数内被调用。每次调用都使用了标准的 C 函数名,但每次调用都必须发生在一个 unsafe 区域内。正如每个新接触 Rust 的程序员所发现的那样,Rust 编译器极度强制内存安全。其他语言(特别是 C 和 C++)作不出相同的保证。unsafe 区域其实是说:Rust 对外部调用中可能存在的不安全行为不负责。

第一个程序输出为:

-123的绝对值是: 123.
9的3次方是: 729.
64的平方根是: 8.
-3.14的平方根是: NaN.

输出的最后一行的 NaN 表示 不是数字 Not a Number :C 库函数 sqrt 期待一个非负值作为参数,这使得参数 -3.14 生成了 NaN 作为返回值。

调用涉及指针的 C 函数

C 库函数为了提高效率,经常在安全、网络、字符串处理、内存管理,以及其他领域中使用指针。例如,库函数 asctime(ASCII 字符串形式的时间)期待一个结构体指针作为其参数。Rust 调用类似 asctime 的 C 函数就会比调用 sqrt 要更加棘手一些,后者既没有牵扯到指针,也不涉及到结构体。

函数 asctime 调用的 C 结构体类型为 struct tm。一个指向此结构体的指针会作为参数被传递给库函数 mktime(时间作为值)。此结构体会将时间拆分成诸如年、月、小时之类的单位。此结构体的 字段 field 类型为 time_t,是 int(32位)和 long(64 位)的别名。两个库函数将这些破碎的时间片段组合成了一个单一值:asctime 返回一个以字符串表示的时间,而 mktime 返回一个 time_t 值表示自 “ 纪元 Epoch 以来所经历的秒数,这是一个系统的时钟和时间戳的相对时间。典型的纪元设置为 1900 年或 1970 年,1 月 1 日 0 时 0 分 0 秒。(LCTT 校注:Unix、Linux 乃至于如今所有主要的计算机和网络的时间纪元均采用 1970 年为起点。)

以下的 C 程序调用了 asctimemktime,并使用了其他库函数 strftime 来将 mktime 的返回值转化成一个格式化的字符串。这个程序可被视作 Rust 对应版本的预热:

#include <stdio.h>
#include <time.h>

int main () {
  struct tm sometime;  /* 时间被打破细分 */
  char buffer[80];
  int utc;

  sometime.tm_sec = 1;
  sometime.tm_min = 1;
  sometime.tm_hour = 1;
  sometime.tm_mday = 1;
  sometime.tm_mon = 1;
  sometime.tm_year = 1; /*LCTT 校注:注意,相对于 1900 年的年数*/
  sometime.tm_hour = 1;
  sometime.tm_wday = 1;
  sometime.tm_yday = 1;

  printf("日期与时间: %s\n", asctime(&sometime));

  utc = mktime(&sometime);
  if( utc < 0 ) {
    fprintf(stderr, "错误: mktime 无法生成时间\n");
  } else {
    printf("返回的整数值: %d\n", utc);
    strftime(buffer, sizeof(buffer), "%c", &sometime);
    printf("更加可读的版本: %s\n", buffer);
  }

  return 0;
}

程序输出为:

日期与时间: Fri Feb  1 01:01:01 1901
返回的整数值: 2120218157
更加可读的版本: Fri Feb  1 01:01:01 1901

(LCTT 译注:如果你尝试在自己电脑上运行这段代码,然后得到了一行关于 mktime 的错误信息,然后又在网上随便找了个在线 C 编译器,复制代码然后得到了跟这里的结果有区别但是没有错误的结果,不要慌,我的电脑上也是这样的。导致本地机器上 mktime 失败的原因是作者没有设置 tm_isdst,这个是用来标记夏令时的标志。tm_isdst 大于零则夏令时生效中,等于零则不生效,小于零标记未知。加入 sometime.tm_isdst = 0= -1 后应该就能得到跟在线编译器大致一样的结果。不同的地方在于结果第一行我得到的是 Mon Feb ...,这个与作者代码中 sometime.tm_wday = 1 对应,这里应该是作者写错了;第二行我和作者和网上得到的数字都不一样,这大概是合理的,因为这与机器的纪元有关;第三行我跟作者的结果是一样的,1901 年 2 月 1 日也确实是周五,这是因为 mktime 其实会修正时间参数中不合理的地方。至于夏令时具体是如何影响 mktime 这个问题,我能查到的只有 mktime 的计算受时区影响,更底层的原因我也不知道了。)

总的来说,Rust 在调用库函数 asctimemktime 时,必须处理以下两个问题:

  • 将裸指针作为唯一参数传递给每个库函数。
  • 把从 asctime 返回的 C 字符串转化为 Rust 字符串。

Rust 调用 asctime 和 mktime

工具 bindgen 会根据类似 math.htime.h 之类的 C 头文件生成 Rust 支持的代码。下面这个简化版的 time.h 就可以用来做例子,简化版与原版主要有两个不同:

  • 内置类型 int 被用来取代别名类型 time_t。工具 bindgen 可以处理 time_t 类,但是会生成一些烦人的警告,因为 time_t 不符合 Rust 的命名规范:time_t 以下划线区分 timet;Rust 更偏好驼峰式命名方法,比如 TimeT
  • 出于同样的原因,这里选择 StructTM 作为 struct tm 的别名。

以下是一份简化版的头文件,mktimeasctime 在文件底部:

typedef struct tm {
  int tm_sec;  /* 秒 */
  int tm_min;  /* 分钟 */
  int tm_hour;   /* 小时 */
  int tm_mday;   /* 日 */
  int tm_mon;  /* 月 */
  int tm_year;   /* 年 */
  int tm_wday;   /* 星期 */
  int tm_yday;   /* 一年中的第几天 */
  int tm_isdst;  /* 夏令时 */
} StructTM;

extern int mktime(StructTM*);
extern char* asctime(StructTM*);

bindgen 安装好后,mytime.h 作为以上提到的头文件,以下命令(% 是命令行提示符)可以生成所需的 Rust 代码并将其保存到文件 mytime.rs

% bindgen mytime.h > mytime.rs

以下是 mytime.rs 中的重要部分:

/* automatically generated by rust-bindgen 0.61.0 */

#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct tm {
  pub tm_sec: ::std::os::raw::c_int,
  pub tm_min: ::std::os::raw::c_int,
  pub tm_hour: ::std::os::raw::c_int,
  pub tm_mday: ::std::os::raw::c_int,
  pub tm_mon: ::std::os::raw::c_int,
  pub tm_year: ::std::os::raw::c_int,
  pub tm_wday: ::std::os::raw::c_int,
  pub tm_yday: ::std::os::raw::c_int,
  pub tm_isdst: ::std::os::raw::c_int,
}

pub type StructTM = tm;

extern "C" {
  pub fn mktime(arg1: *mut StructTM) -> ::std::os::raw::c_int;
}

extern "C" {
  pub fn asctime(arg1: *mut StructTM) -> *mut ::std::os::raw::c_char;
}

#[test]
fn bindgen_test_layout_tm() {
  const UNINIT: ::std::mem::MaybeUninit<tm> = ::std::mem::MaybeUninit::uninit();
  let ptr = UNINIT.as_ptr();
  assert_eq!(
  ::std::mem::size_of::<tm>(),
  36usize,
  concat!("Size of: ", stringify!(tm))
  );
  ...

Rust 结构体 struct tm,跟原本在 C 中的一样,包含了 9 个 4 字节的整型字段。这些字段名称在 C 和 Rust 中是一样的。extern "C" 区域声明了库函数 astimemktime 分别需要只一个参数,一个指向可变实例 StructTM 的裸指针。(库函数可能会通过指针改变作为参数传递的结构体。)

#[test] 属性下的其余代码是用来测试 Rust 版的时间结构体的布局。通过命令 cargo test 可以进行这些测试。问题在于,C 没有规定编译器应该如何对结构体中的字段进行布局。比如说,C 的 struct tm 以字段 tm_sec 开头用以表示秒;但是 C 不需要编译版本遵循这个排序。不管怎样,Rust 测试应该会成功,而 Rust 对库函数的调用也应如预期般工作。

设置好第二个案例并开始运行

bindgen 生成的代码不包含 main 函数,所以是一个天然的模块。以下是一个 main 函数初始化了 StructTM 并调用了 asctimemktime

mod mytime;
use mytime::*;
use std::ffi::CStr;

fn main() {
  let mut sometime  = StructTM {
    tm_year: 1,
    tm_mon: 1,
    tm_mday: 1,
    tm_hour: 1,
    tm_min: 1,
    tm_sec: 1,
    tm_isdst: -1,
    tm_wday: 1,
    tm_yday: 1
  };

  unsafe {
    let c_ptr = &mut sometime; // 裸指针

    // 调用,转化,并拥有
    // 返回的 C 字符串
    let char_ptr = asctime(c_ptr);
    let c_str = CStr::from_ptr(char_ptr);
    println!("{:#?}", c_str.to_str());

    let utc = mktime(c_ptr);
    println!("{}", utc);
  }
}

这段 Rust 代码可以被编译(直接用 rustc 或使用 cargo)并运行。输出为:

Ok(
    "Mon Feb  1 01:01:01 1901\n",
)
2120218157

对 C 函数 asctimemktime 的调用必须再一次被放在 unsafe 区域内,因为 Rust 编译器无法对这些外部函数的潜在内存安全风险负责。此处声明一下,asctimemktime 并没有安全风险。调用的两个函数的参数是裸指针 ptr,其指向结构体 sometime (在 stack 中)的地址。

asctime 是两个函数中调用起来更棘手的那个,因为这个函数返回的是一个指向 C char 的指针,如果函数返回 Mon 那么指针就指向 M。但是 Rust 编译器并不知道 C 字符串 (char 的空终止数组)的储存位置。是内存里的静态空间?还是 heap asctime 函数内用来储存时间的文字表达的数组实际上是在内存的静态空间里。无论如何,C 到 Rust 字符串转化需要两个步骤来避免编译错误:

  • 调用 Cstr::from_ptr(char_ptr) 来将 C 字符串转化为 Rust 字符串并返回一个引用储存在变量 c_str 中。
  • c_str.to_str() 的调用确保了 c_str 是所有者。

Rust 代码不会增加从 mktime 返回的整型值的易读性,这一部分留作课外作业给感兴趣的人去探究。Rust 模板 chrono::format 也有一个 strftime 函数,它可以被当作 C 的同名函数来使用,两者都是获取时间的文字表达。

使用 FFI 和 bindgen 调用 C

Rust FFI 和工具 bindgen 都能够出色地协助 Rust 调用 C 库,无论是标准库还是第三方库。Rust 可以轻松地与 C 交流,并透过 C 与其他语言交流。对于调用像 sqrt 一样简单的库函数,Rust FFI 表现直截了当,这是因为 Rust 的原始数据类型覆盖了它们在 C 中的对应部分。

对于更为复杂的交流 —— 特别是 Rust 调用像 asctimemktime 一样,会涉及到结构体和指针的 C 库函数 —— bindgen 工具是优秀的帮手。这个工具会生成支持代码以及所需要的测试。当然,Rust 编译器无法假设 C 代码对内存安全的考虑会符合 Rust 的标准;因此,Rust 必须在 unsafe 区域内调用 C。


via: https://opensource.com/article/22/11/rust-calls-c-library-functions

作者:Marty Kalin 选题:lkxed 译者:yzuowei 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

理解 OSI 框架的基本概念,掌握计算机系统通信机制。

开放系统互联 Open Systems Interconnection (OSI)模型是一个定义计算机、服务器和用户如何在一个系统内通信的标准。它是第一个网络通信标准模型,在上世纪 80 年代早期,所有主流的计算机和通信公司都采用了这个标准。

OSI 模型提供了一种用于描述网络的通用语言,并以离散的块或层的方式来描述。

OSI 模型的各个层

该模型描述了计算机系统通过网络进行通信的七个层。

  • 7 应用层
  • 6 表示层
  • 5 会话层
  • 4 传输层
  • 3 网络层
  • 2 数据链路层
  • 1 物理层

每个层都有自己的工作方式和一系列跟其他层不同的协议。本文将逐个剖析这些层级。

应用层

应用层是在软件中实现的。它是与应用程序交互的层级。

用发送消息作为例子。发送消息的程序与应用层进行交互,并发送消息。接着,应用层向 OSI 模型的下一个层级(即表示层)发送消息。

表示层

来自应用层的数据被转发到表示层。表示层接收到文字、字符、字母、数字等形式的数据,并把它们转换为机器可识读的二进制格式数据。这个过程叫做编译。

在此阶段,ASCII(美国信息交换标准码)字符被转换为扩充的二进制编码的十进制交换码(EBCDIC)。转换后的数据在继续传输前,也会进行编码和加密过程,使用 SSL 协议进行加密和解密。

表示层的作用是抽象化,它假设下面的层级会处理它们收到的数据。它也负责压缩数据。数据的压缩可能是有损的,也有可能是无损的,这取决于很多因素,这不属于本文的讨论范围。

会话层

会话层的作用是建立和管理连接。该层级的主要工作是建立会话。例如,你登录网上商城,就在你的机器和服务器之间建立了会话。

会话层的作用是实现数据的发送和接收,完成后连接的会话就终止了。在一个会话建立前,会进行身份验证。与上一层类似,会话层也假设在它的工作完成后,下面的层级也会准确无误地处理数据。

传输层

传输层的作用是管理数据传输和其自身的关于数据如何传输的一些协议。从会话层传到这里的数据被分为更小的数据单元,这些数据单元称为片段。这个过程叫做“分段”。每个片段包含来源端口号、目标端口号和一个序列号。端口号用来识别发送数据的应用程序。注意,数据以块的形式传输。序列号用于把这些片段按正确的顺序排列。

传输层负责控制流量或在给定的时间内传输的数据量。它也负责错误控制,比如数据丢失、损坏等情况。它利用一种错误检测值,通常叫做校验和。传输层对每个数据片段加上校验和,就可以检查所发送的数据是否被正确接收。然后数据传输到网络层。

网络层

网络层的作用是跟其他网络进行通信。它把从一台机器接收到的数据片段传输给另一台位于不同网络的机器。路由器是作用于网络层的。

网络层的功能是逻辑寻址(就是确定 IP 地址)。它为发送方和接收方分配 IP 地址,数据包附带了这个地址,就可以被传输到正确的目标机器。接着网络层对数据包进行路由。负载均衡也是在网络层进行的,旨在确保不会发生过载。下一步,数据传输到数据链路层。

数据链路层

数据链路层支持跟其他设备直接通信。

它接收到来自网络层、包含发送方和接收方 IP 地址的数据包,进行物理寻址,然后将发送方和接收方的 MAC 地址分配给数据包,形成帧。

物理层

物理层由系统的所有硬件和物理设备(包括网线、导航系统、适配器等)组成。在这里,从前面层级接收到的数据都是 0 和 1 形式的。物理层把这些数据转换并通过各种方式(如果是光纤电缆,有电线、电信号、光信号;如果是 WIFI,则为无线电信号)传输至本地媒介。

注意,物理层作用于接收方的一端,把接收到的信号以帧的形式传输到数据链路层(把它转换回二进制数据形式)。然后帧传输到上面的层级,最终应用层(应用软件)会接收到需要的数据。

结语

当你需要描述网络架构或排除网络问题时,OSI 模型的相关知识会对你有所帮助。我希望本文能令你对这个模型的方方面面有清晰的理解。


via: https://opensource.com/article/22/10/osi-model-network-communications

作者:Anamika 选题:lkxed 译者:cool-summer-021 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

Linux Mint 是一款极好的 Linux 发行版,特别适合初学者。

我喜欢它仍然保持常见的 Ubuntu/Debian 习惯,但是它还做了一些 比 Ubuntu 更好的 工作,其中之一就是它没有使用 Snap。

然而,我不是 Cinnamon 桌面环境的粉丝,因为我从来没有真正地喜欢过 Windows XP 或 7 的默认设置。

当我寻求保持 Linux Mint 稳定的同时而提供 GNOME 的能力时,这便是我最终获得的结果:

install gnome in linux mint

不太炫,这就是我运行 GNOME 42.5 的 Linux Mint 21 。

如果你想在 Linux Mint 上安装 GNOME ,那么这篇指南非常适合你。

在 Linux Mint 上安装GNOME 之前所要知道的事

要在 Mint 上安装 GNOME,你务必需要有足够的理由。如果你只是为了尝鲜,可以在虚拟机中尝试。我使用 在 VirtualBox 中安装的 Linux Mint 来演示这篇教程。

安装除发行版提供的桌面环境之外的其他桌面环境,移除桌面环境部分会使其变成一件很复杂的事。

Cinnamon 使用了一些 GNOME 元素。如果你决定稍后移除 GNOME ,这可能会影响到 Cinnamon 的一部分功能。

这可能会导致缺少实战经验用户的恐慌。当然,在 TTY 屏幕中重新安装 Cinnamon 桌面环境可能是一种可行的解决方案。

最重要的一点是,如果你很容易惊慌地不知所措和不喜欢解决难题,那么你就不应该在你的主力计算机上做这些 “试验” 。

抛开这些顾虑,让我们看看在 Linux Mint 上获取 GNOME 的简单过程。

在 Linux Mint 上安装 GNOME 桌面环境

在这里,你有两个选项:1、你可以使用包含所有的 GNOME 实用程序的完整的 GNOME 桌面,2、你也可以使用包含极少数软件包的 GNOME 精简版本。

我都将讲解一下。

安装精简版本的 GNOME ,你需要安装一个名称为 vanilla-gnome-desktop 的软件包,使用下面给定的命令:

sudo apt install vanilla-gnome-desktop

如果你想要完整的 GNOME 体验 ,你可以简单地安装 gnome 软件包:

sudo apt install gnome

在你执行上述任一个命令后,在接下来的步骤中将会要求你选择首选的显示管理器。

choose display manager

gdm3 是 GNOME 桌面的显示管理器,而 Linux Mint 使用 lightdm 作为默认的显示管理器,这两种显示器都可以正常工作,但是,我建议你使用 gdm3 来获取完整的 GNOME 体验。

切换到 GNOME

在完成后,注销并按一次回车键,在这里,你将看到一个小齿轮图标。从这里选择 “GNOME” :

choose gnome while logging in

现在,你拥有以 Linux Mint 为基础的 GNOME 桌面环境!

额外提示:如何应用整体风格一致的主题

你可以继续使用 Cinnamon 桌面的主题,但是它们大多不能如前工作,因此,我建议使用 GNOME 桌面的主题(例如 Adwaita )来保持桌面环境的一致性。

对我而言,其默认的字体没有一点效果。并且,我更喜欢 Fedora 提供的一些字体。因此,从系统菜单打开 GNOME 调整 GNOME tweaks ,并作出如下更改:

change fonts in ubuntu to have vanilla gnome experience

这里是我使用的一些设置:

  • Cantarell Regular (11) 用于界面和文档文本。
  • Noto Sans Mono Regular (13) 用于等宽字体文本。
  • Cantarell Bold (11) 用于窗口标题。

它们的结果是,比默认的 Ubuntu 字体方案要好得多。

既然你有了 GNOME ,你可以使用我们的详细指南来安装和 更改 Linux 上的 GNOME 主题,来使其成为你所梦想的样子。

总结

如你所见,在 Linux Mint 上安装 GNOME 是非常简单的。正如我先前所提到的那样,移除部分可能会使事情复杂化,因为这可能会移除一些 Cinnamon 所需要的一些 GNOME 软件包。

你现在的主力计算机系统是什么?我的是 Pop!\_OS 。


via: https://itsfoss.com/install-gnome-linux-mint/

作者:Sagar Sharma 选题:lkxed 译者:robsean 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

本文将比较非 root 用户提权为 root 用户的两个 Linux 命令 的区别。

susudo 命令都允许用户执行非特权用户不允许做的系统管理任务,即只有 root 用户能执行的命令。有些人更喜欢 sudo 命令:例如 Seth Kenlon 最近发布的一篇 《在 Linux 上使用 sudo 的 5 个理由》,他在其中详细阐述了 sudo 命令的许多优点。

但是,相较于 sudo 命令,我更偏好于 su 命令 来做系统管理工作。在本文中,我比较了这两个命令的区别,并解释了为什么我更喜欢 su 而不是 sudo,但我仍然同时使用这两个命令的原因。

过去的系统管理员主要使用 su 命令

susudo 命令是为不同的世界设计的。早期的 Unix 计算机需要全职系统管理员,他们使用 root 用户作为唯一的管理帐户。在这个古老的世界里,有管理员密码的人会在电传打字机或 CRT 终端(例如 DEC VT100)上以 root 用户登录,然后执行一些管理 Unix 计算机的工作。

管理员还会有一些非 root 帐户,用于执行一些非 root 的任务,例如编写文档和管理电子邮件等。在这些 Unix 计算机上通常有许多非 root 帐户,他们都不需要完全的 root 访问权限,只需要以 root 权限运行很少的命令,大约 1 至 2 个就可以了。许多系统管理员以 root 用户登录,完成 root 工作,并在任务完成后,退出 root 会话。有时候,系统管理员需要整天以 root 用户来登录,因为 sudo 命令需要键入更多的内容才能运行必要的命令,因此大多数系统管理员很少使用 sudo 命令。

sudosu 这两个命令都能够提权为 root 用户,但它们实现的方式大不相同。这种差异是由于它们最初打算用于不同的情况

sudo 命令

sudo 命令的初衷是让 root 用户能够让几个非 root 用户访问他们经常需要的一两个特权命令。sudo 命令允许非 root 用户暂时地获得更高权限,来执行一些特权命令,例如添加和删除用户、删除属于其他用户的文件、安装新软件以及管理现代 Linux 主机所需的任何命令。

sudo 命令允许非 root 用户访问一两个 需要更高权限 的常用命令,这样可以帮助系统管理员节省来自用户的许多请求,并减少等待时间。sudo 命令不会将用户帐户切换为 root 用户,因为大多数非 root 用户永远不应该拥有完全的 root 访问权限。在大多数情况下,sudo 允许用户执行一两个命令,然后提权就会过期。在这个通常为 5 分钟的短暂的提权时间内,用户可以执行任何需要提权的管理命令。需要继续使用提权的用户可以运行 sudo -v 命令来重新验证 root 访问权限,并将提权时间再延长 5 分钟。

使用 sudo 命令还有一些副作用,例如生成非 root 用户使用命令的日志条目及其 ID。这些日志可以在之后作为出现问题的检验,来给用户更多的操作培训。(你以为我会说“问责”用户,对吧?)

su 命令

su 命令能够将非 root 用户提权到 root 权限 —— 事实上,能让非 root 用户成为 root 用户。唯一的要求是用户知道 root 密码。因为用户已经以 root 权限登录,所以之后的操作就没有限制了。

su 命令所提供的提权没有时间限制。用户可以作为 root 执行命令,不需要进行重新验证是否有 root 权限。完成任务后,用户可以执行退出命令 exit,从 root 用户恢复到自己原来的非 root 帐户。

su 和 sudo 在使用上的争议和变化

最近在 susudo 的使用上存在一些分歧。

真正的系统管理员不会使用 sudo。—— Paul Venezia

Venezia 在他的 InfoWorld 文章 中辩称,对于许多担任系统管理员的人来说,sudo 是一个不必要的工具。他没有花太多时间为这个观点进行解释,他只是把它说成了一个事实。我同意他对于系统管理员的观点,因为我们不需要 sudo 来完成我们的工作。事实上,sudo 使得事情变得更复杂了。

然而,

时代在“改变”。—— Bob Dylan

Bob Dylan 是对的,尽管他唱的歌不是指计算机(LCTT 译注:Bob Dylan 是美国创作歌手、艺术家和作家,这里指他不是针对于电脑而说的)。

自从人手一台的个人计算机时代到来,计算机的管理方式发生了重大变化。在许多环境中,计算机的使用者也是它的管理员,这使得为这些用户提供一些对 root 权限的访问是有必要的。

一些现代发行版,例如 Ubuntu 及其衍生版本,只能使用 sudo 命令来执行特权命令。在这些发行版中,用户无法直接以 root 用户身份登录,甚至无法通过 su 切换到 root,因此需要 sudo 命令来允许非 root 用户获得 root 权限。在这一环境中,所有系统管理任务均使用 sudo 来执行。

通过锁定 root 帐户并将常规用户帐户添加到“轮子”组(wheel),可以实现此配置,但是这种配置很容易被绕过。接下来,让我们在 Ubuntu 主机或虚拟机上尝试一些小实验吧。我在这里说明一些我的设置,以便你可以根据需要来重现它。我安装的是 Ubuntu 16.04 LTS1,并使用 VirtualBox 将其安装在虚拟机中。在安装过程中,我创建了一个非 root 用户 student,为了简便起见我给这个用户设置了一个简单的密码。

student 用户身份登录 Ubuntu,并打开终端。查看 /etc/shadow 文件中的 root 条目,其中存储了经哈希的密码。

student@ubuntu1:~$ cat /etc/shadow
cat: /etc/shadow: Permission denied

可以看到终端拒绝了我们对 /etc/shadow 的访问,因此我们无法查看 /etc/shadow 文件。所有发行版都是如此,以防止非特权用户看到和访问加密的密码,因为非特权用户可能会使用常见的黑客工具来破解这些密码。

现在,让我们使用 su - 命令来成为 root 用户。

student@ubuntu1:~$ su -
Password: <Enter root password – but there isn't one>
su: Authentication failure

认证失败的原因是因为 root 帐户没有密码、并且被锁定了。接下来,使用 sudo 命令查看 /etc/shadow 文件。

student@ubuntu1:~$ sudo cat /etc/shadow
[sudo] password for student: <enter the student password>
root:!:17595:0:99999:7:::
<截取>
student:$6$tUB/y2dt$A5ML1UEdcL4tsGMiq3KOwfMkbtk3WecMroKN/:17597:0:99999:7:::
<截取>

在这里,我仅截取了部分结果,只显示 root 和 student 用户的条目。我还缩短了加密密码,以便该条目能显示在一行中。各个字段以冒号(:)分隔,第二个字段是密码。请注意,root 的密码字段是一个感叹号(!),这表明 root 帐户已被锁定,且无法使用。

现在,要将 root 帐户变成一个合适的系统管理员,你只需为 root 帐户设置密码。

student@ubuntu1:~$ sudo su -
[sudo] password for student: <Enter password for student>
root@ubuntu1:~# passwd root
Enter new UNIX password: <Enter new root password>
Retype new UNIX password: <Re-enter new root password>
passwd: password updated successfully
root@ubuntu1:~#

现在,你可以直接以 root 身份登录到控制台,或者直接使用 su 登录到 root,而不是在每个命令前都加一个 sudo。当然,你也可以在每次想以 root 身份登录时,使用 sudo su -,但这又是何必呢?

请不要误解我的意思。像 Ubuntu 这样的发行版及其上下游衍生版非常好,多年来我已经使用了其中的几个。在使用 Ubuntu 和相关发行版时,我做的第一件事就是设置一个 root 密码,这样我就可以直接以 root 身份登录。其他发行版,如 Fedora 及其相关发行版,现在在安装过程中提供了一些有趣的选择。我注意到的第一个 Fedora 版本是 Fedora 34,我在写我的一本即将出版的书时安装了很多次。

在安装页面上,可以找到其中一个安装选项,来设置 root 密码。这个新选项允许用户选择“ 锁定 root 帐户 Lock root account ”,就像 Ubuntu 锁定 root 帐户的方式一样。此页面上还有一个选项,允许使用密码以 root 身份远程 SSH 登录到此主机,但这仅在 root 帐户解锁时有效。第二个选项位于允许创建非 root 帐户的页面上。此页面上的选项之一是“ 让此用户成为管理员 Make this user administrator ”。选中此选项后,用户 ID 将添加到一个名为 wheel 组的特殊组中,该组授权该组的成员使用 sudo 命令。Fedora 36 甚至在该复选框的描述中提到了 wheel 组。

可以将多个非 root 用户设置为管理员。使用此方法指定为管理员的任何人都可以使用 sudo 命令在 Linux 计算机上执行所有管理任务。Linux 在安装时只允许创建一个非 root 用户,所以其他新用户可以在创建时添加到 wheel 组中。root 用户或其他管理员可以使用文本编辑器或 usermod 命令直接将现有用户添加到 wheel 组。

在大多数情况下,今天的管理员只需要执行一些基本任务,例如添加新的打印机、安装更新或新软件,或者删除不再需要的软件。这些 GUI 工具需要 root 或管理密码,并将接受来自管理员用户的密码。

在 Linux 上,我是怎么使用 su 和 sudo 的呢

同时使用 susudo。它们都是我所使用的很重要的系统管理员工具。

我不锁定 root 帐户,因为我需要用 root 帐户来运行我的 Ansible 脚本和我编写的 rsbu Bash 程序,来执行备份。这两个程序都需要以 root 身份运行,我编写的其他几个管理 Bash 的脚本也是如此。我使用 su 命令切换到 root 用户,这样我就可以执行这些脚本和许多其他常见的命令。当我需要确定问题和解决问题时,使用 su 命令将我的权限提升到 root 十分有用,因为我不希望 sudo 带来的提权会话超时。

当非 root 用户需要执行这些任务时,我使用 sudo 命令,来执行需要 root 权限的任务。我在 sudoers 文件中设置了非 root 帐户,只允许访问完成任务所需的一两个命令。当我只需要运行一两个需要提权的快速命令时,我自己也会使用 sudo 命令。

结论

实际上只要你把工作完成好了,你使用什么工具都无大碍。你使用的是 Vim 还是 Emacs,是 systemd 还是 SystemV,是 RPM 亦或是 DEB,是 sudo 亦或是 su,在结果上会有什么区别呢?这里的关键在于你应该使用最适合你的工具。Linux 和开源软件的最大优势之一是通常有许多选项可用于我们需要完成的任务。

susudo 都各有长处,如果正确使用的话,两者都是非常安全的。我选择同时使用 susudo 命令,基于它们的历史功能,因为这对我来说十分有用。对于我自己的大部分工作,我更喜欢 su 命令,因为它与我的工作流程最适配。

在评论区分享你喜欢的工作方式吧!

本文摘自于我的书《 系统管理员的 Linux 哲学 The Linux Philosophy for Sysadmins (Apress,2018 年)》一书的第 19 章,并经许可后重新发布。


via: https://opensource.com/article/22/6/linux-su-vs-sudo-sysadmin

作者:David Both 选题:lkxed 译者:chai001125 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

一个简单的教学展示,教你如何找到正在运行中的进程 ID 并杀死它,你可以使用终端或者 GUI,这个方法适用于各类 Linux 发行版。

你的 Linux 系统中运行的应用可能会让你的电脑变慢,特别是你的电脑配置较低的时候。在 Linux (以及所有其他 OS)中,程序或者应用都携带一个特别的 PID (进程 ID)可供你简单地分辨它们。

然而,大部分 Linux 初学者用户并不知道如何在 Linux 中寻找运行中的进程并杀死它。在这篇指南中,我们将会解释用不同的方法以杀死 Linux 中的运行进程。这包括了使用终端和 GUI 的方法。

记住,你只应该杀死未响应的进程,或者你发现应用无法被正常关闭 (针对基于 GUI 的应用)。

如何在 Linux 中找到 PID 并杀掉它们

在这一部分中,我们首先应该先学会如何找到运行进程的 PID,然后再学习用以杀掉它们的命令:

找到正在运行中的进程

你可以使用命令 top 来列出所有正在进行中的进程和它们的 PID,以及其他细节。程序 top 在所有 Linux 发行版和所有基于 Unix 的系统中都是默认安装了的。

top

Top program output

同样地,你可以执行命令 ps 附带额外选项来获取某个指定的进程的 PID。例如,你可以使用以下命令来获得 firefox 的 PID。

ps -el | grep -i firefox

Firefox process id using ps command - example

现在你已经找到 PID 了,让我们看看你该如何杀掉它。

杀死运行中的进程

使用以下命令,你可以通过进程的名字或者 PID 来杀掉这个正在运行中的进程:

  • killall:通过运行进程的名字来杀死进程
  • kill:通过 PID 来杀死进程

现在,让我们首先使用进程 killall 通过 Firefox 这个名字来杀死它的,命令如下:

killall -9 firefox
  • 参数 -9 发送了信号 SIGKILL 通知 OS 来终止这个进程。
  • 使用以下命令,你也可以列出一些别的信号。
kill -l

同样地,如果你想要通过 PID 来杀死进程,你可以用以下命令:

kill -9 <PID>

在这个例子中,命令会长这样:

kill -9 33665

让我们看看在不同发行版中,你该如何使用图形用户界面(GUI)来杀死任意进程或应用。

通过 GUI 寻找 PID 并杀掉

现在有很多图形界面程序可以枚列进程。大部分 Linux 发行版的桌面环境中已经携带了它们。我们在这里列举出了一些。

GNOME(在 Ubuntu、Fedora 工作站等) & 在 Linux Mint 中

在应用菜单中搜索 “system monitor” 并打开它(LCTT 译注:中文桌面环境也可以搜 “system monitor”,我在 Ubuntu 里试过了)。在 “ 进程 Processes ” 标签页下找到你的进程,右击进程名字打开快捷菜单,选择选项 “ 杀死 Kill ”。

Kill a process in Linux using gnome system monitor

KDE Plasma(Kubuntu、Fedora-KDE 或任何基于 Plasma 的发行版)

在应用菜单中搜索并启动 “system monitor”。这会打开以下程序。在左边菜单栏点击“ 进程 Processes ” ,你因该能看见一列正在运行的程序。你可以右击列表里的进程或应用并选择“ 杀死 Kill ”来终止进程。

System monitor in KDE Plasma

Xfce 桌面

Xfce 桌面可以完成这项任务的原生应用是 任务管理器 Task Manager ,你可以通过 “ 应用 Application > 系统 System > 任务管理器 Task manager ” 来找到它。右击进程名字然后选择“ 杀死 Kill ”来终止应用或进程。

Xfce task manager to kill a process

如何在其他桌面或发行版上杀死一个进程或程序

如果你找不到任何相似的程序,你可以选择使用终端的方法。或者,你可以使用以下命令来安装 gnome-system-monitor。

Ubuntu 以及相关发行版:

sudo apt install gnome-system-monitor

Fedora 以及其相关的发行版:

sudo dnf install gnome-system-monitor

还有 Arch Linux:

sudo pacman -S gnome-system-monitor

总结一下

这就是你该如何在 Linux 中找到一个运行中的进程的 PID 并杀死它。我们已经解释了不同的方法:你可以通过名字或者 PID 来杀死进程。我希望这对你有所帮助。


via: https://www.debugpoint.com/find-process-id-kill-linux/

作者:Arindam 选题:lkxed 译者:yzuowei 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

Nemo 感觉像是更新版的 GNOME 2 文件管理器。我喜欢它,我觉得你也会喜欢它。

计算机是一个奇特的文件柜,里面装满了虚拟文件夹和文件,等待着被引用、交叉引用、编辑、更新、保存、复制、移动、重命名和归类。在本文中,我将介绍一下 Linux 系统的文件管理器。

Cinnamon 项目是使用 GNOME 3 的组件重新实现的 GNOME 2。最终,它的差异足以成为一个真正的分叉,如今,Cinnamon 桌面使用 GTK3 库和 GNOME 3 关键应用的分叉版本来创建一个“经典”的 GNOME 体验。对传统的 GNOME 体验做出贡献的组件之一是 Nemo,它是一个基于 GNOME 2 版本的 Nautilus 的文件管理器。

在 Linux 上安装 Nemo

Nemo 的源代码 在线提供,但它需要 cinnamon-desktop 来构建,所以安装 Nemo 最简单的方法是直接安装 Cinnamon。

在 Fedora、Mageia 和类似的系统上:

$ sudo dnf install cinnamon-desktop

在 Linux Mint、Debian 和类似系统上:

$ sudo apt install cinnamon-desktop-environment

当然,作为该桌面的发源地,Linux Mint 也预装了 Cinnamon。

一个熟悉的界面

如果你已经习惯了 GNOME,无论是过去的还是现在的,那么 Nemo 从一开始就让你有一种回家的感觉。它有一个熟悉的外观和感觉,在类似的地方有按钮和选项,你很可能会认出它们。

Image of Nemo's file manager.

将视图控制按钮放在右上方的 GNOME 式惯例被保留了下来,你可以使用这些按钮快速地将你的文件视图从大图标切换到详细列表或紧凑视图。那里还有一个搜索功能,以及在可编辑文本和按钮之间切换位置栏的选项。

可编辑的 URI 栏有时被低估了。这是一个简单的设计决定,但它可以是一个有助于提高效率的巨大功能。这就像在每个窗口的顶部有一个单行终端,你可以在那里输入你的系统的任何目标位置,并立即被带到那里。而且你甚至不需要输入 cd

在左上角,有导航按钮:向上、向前和向后。与许多 Linux 文件管理器一样,你可以用 Alt 键加上适当的 箭头 键,而放弃使用这些按钮。

侧面窗格显示了重要文件夹的列表(主目录、文档、下载等),可以通过点击窗口底部的一个按钮来隐藏或显示。

熟悉但不一样

Nemo 的舒适性和熟悉性并不意味着它只是无意识地模仿 Nautilus。Nemo 有一系列不错的功能,感觉很独特。其中大部分都在 偏好设置 Preferences 中,以下是我最喜欢的几个:

  • 窗口标题中显示全路径 Full path in window title :这是我最喜欢的功能。不要再怀疑你在文件系统中的位置了。让你的窗口标题告诉你。
  • 单击或双击 Single or double click :如果你是一个长期的 KDE 用户,你可能会发现单次点击打开一个文件是很新鲜的。有了 Nemo,你可以这样选择。
  • 双击来重命名 Double-click to rename :如果你使用单击来打开,为什么不重新利用双击来重命名呢?
  • 在新窗口中打开每个文件夹 Open each folder in a new window :操作系统为每一个打开的文件夹打开一个新的窗口。
  • 插件 Plugins :Nemo 有能力调用动作、脚本和扩展。有些已经包括在内,有改变桌面背景、创建启动器和挂载归档的动作。其他的还没有被创建,但这种可扩展性对开源是至关重要的。

一切近在咫尺

在 Linux Mint 上使用 Nemo 几周后,一个有趣的特征让我眼前一亮。似乎 Nemo 可以,或者通过快速配置可以,让我最常使用的所有东西都近在咫尺。许多功能,我承认,我不知道我是否需要,直到 Nemo 让我轻松点击才知道。你可能会说,我是为了满足 Nemo 的设计而改变了我的使用方式,也许情况确实如此。但是,当这种体验是如此令人愉快和有效时,这有什么关系呢?

Nemo 是一个很好的文件管理器。它让人想起了 GNOME 2 的时代,但其更新和设计选择让人有一种新鲜的感觉。如果你喜欢 Thunar 或 Nautilus,你会喜欢 Nemo。


via: https://opensource.com/article/22/12/linux-file-manager-nemo

作者:Seth Kenlon 选题:lkxed 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出