2. 系统调用I/O编程¶
系统调用(System Call)是Linux内核向上层提供的底层I/O操作接口,直接运行在内核态,是一切上层I/O操作的基础。 相较于标准I/O的封装性与跨平台性,系统调用I/O更贴近硬件、效率更高,能精准把控I/O流程,是Linux底层开发、高性能程序编写的核心技能。
实际上,Linux提供的系统调用包含以下内容:
进程控制:如fork、clone、exit 、setpriority等创建、中止、设置进程优先级的操作。
文件系统控制:如open、read、write等对文件的打开、读取、写入操作。
系统控制:如reboot、stime、init_module等重启、调整系统时间、初始化模块的系统操作。
内存管理:如mlock、mremap等内存页上锁重、映射虚拟内存操作。
网络管理:如sethostname、gethostname设置或获取本主机名操作。
socket控制:如socket、bind、send等进行TCP、UDP的网络通讯操作。
用户管理:如setuid、getuid等设置或获取用户ID的操作。
进程间通信:包含信号量、管道、共享内存等操作。
从逻辑上来说,系统调用可被看成是一个Linux内核与用户空间程序交互的中间人,它把用户进程的请求传达给内核, 待内核把请求处理完毕后再将处理结果送回给用户空间。它的存在就是为了对用户空间与内核空间进行隔离, 要求用户通过给定的方式访问系统资源,从而达到保护系统的目的。
也就是说,我们心心念念的Linux应用程序与硬件驱动程序之间就是各种各样的系统调用, 所以无论出于何种目的,系统调用是学习Linux开发绕不开的话题。
本章的示例代码目录为:base_linux/file_io/systemcall
2.1. 文件描述符¶
文件描述符是Linux系统中用于标识打开文件、设备、管道、套接字等I/O资源的唯一整数, 是系统调用I/O的核心操作对象,贯穿整个底层I/O流程,理解文件描述符是掌握系统调用I/O的前提。
2.1.1. 文件描述符的本质¶
文件描述符是进程PCB(进程控制块)中,文件描述符表的索引值,属于非负整数。 每个进程启动后,内核会为其分配独立的文件描述符表,用于记录该进程打开的所有I/O资源,文件描述符就是指向表中对应项的下标。
文件描述符分配规则如下:
文件描述符从0开始递增分配,遵循“最小未使用”原则,内核会优先分配当前最小的空闲整数;
每个进程默认打开3个标准文件描述符,由系统自动分配,无需手动打开/关闭;
进程关闭文件描述符后,对应整数会被回收,可重新分配给新打开的资源;
单个进程可打开的文件描述符数量有限制,可通过系统配置修改。
2.1.2. 进程默认标准文件描述符¶
所有Linux进程启动时,内核都会自动打开3个标准I/O流,对应固定的文件描述符,是进程与终端交互的基础, 几种文件描述符如下:
文件描述符值 |
宏定义 |
对应标准流 |
|---|---|---|
0 |
STDIN_FILENO |
标准输入流 |
1 |
STDOUT_FILENO |
标准输出流 |
2 |
STDERR_FILENO |
标准错误流 |
这三个文件描述符遵循“先入为主”原则,即使关闭后重新分配,数值仍对应原有标准流功能;
2.1.3. 文件描述符的特性¶
进程独立性:不同进程的文件描述符相互独立,相同数值的文件描述符,在不同进程中指向不同I/O资源;
资源唯一性:一个文件描述符同一时间只能指向一个I/O资源,一个资源可被多个文件描述符指向;
生命周期绑定进程:进程退出后,内核会自动回收该进程所有文件描述符,释放对应I/O资源;
通用性:文件描述符不仅可操作普通文件,还能操作管道、套接字、设备文件、共享内存等所有Linux“文件型”资源,贴合“一切皆文件”的设计哲学。
2.1.4. 文件描述符相关常用命令¶
可通过shell命令查看进程文件描述符使用情况,辅助排查资源泄漏问题:
1 2 3 4 5 6 7 8 | # 查看当前进程可打开的最大文件描述符数
ulimit -n
# 查看指定进程的文件描述符列表,PID需改为实际PID
ls -l /proc/PID/fd
# 查看系统全局最大文件描述符限制
cat /proc/sys/fs/file-max
|
2.2. 系统调用I/O核心函数¶
系统调用I/O的核心操作围绕“打开文件->读写数据->控制文件状态->关闭文件”展开, 对应内核提供的open/close、read/write、lseek、fcntl、ioctl等函数, 所有函数均需包含指定头文件,且调用失败时会设置全局errno变量,便于错误排查。
2.2.1. 文件打开与关闭函数¶
2.2.1.1. open函数¶
open函数是系统调用I/O的入口函数,用于打开已存在文件,或创建新文件,返回对应的文件描述符,是后续所有I/O操作的基础。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // 所需头文件
#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:文件打开标志,必选参数,指定文件操作模式
// mode:文件权限,仅创建新文件时需要,指定新建文件的访问权限
//返回值:成功返回非负整数(文件描述符),失败返回-1,并设置errno。
|
flag参数值如下:
标志位 |
含义 |
|---|---|
O_RDONLY |
以只读的方式打开文件,该参数与O_WRONLY和O_RDWR只能三选一 |
O_WRONLY |
以只写的方式打开文件 |
O_RDWR |
以读写的方式打开文件 |
O_CREAT |
文件不存在则创建,必须搭配mode参数 |
O_EXCL |
与O_CREAT联用,文件已存在则报错,防止重复创建 |
O_TRUNC |
如果pathname文件存在,则清除文件内容 |
O_APPEND |
追加模式,写入数据自动追加至文件末尾 |
O_NONBLOCK |
非阻塞模式,I/O操作无法立即完成时不阻塞进程 |
O_SYNC |
同步写入模式,数据直接写入磁盘,不使用内核缓存 |
当open函数的flag值设置为O_CREAT时,必须使用mode参数来设置文件与用户相关的权限。
mode可用的权限如下表所示,表中各个参数可使用“| ”来组合。
\ |
标志位 |
含义 |
|---|---|---|
当前用户 |
S_IRUSR |
用户拥有读权限 |
\ |
S_IWUSR |
用户拥有写权限 |
\ |
S_IXUSR |
用户拥有执行权限 |
\ |
S_IRWXU |
用户拥有读、写、执行权限 |
当前用户组 |
S_IRGRP |
当前用户组的其他用户拥有读权限 |
\ |
S_IWGRP |
当前用户组的其他用户拥有写权限 |
\ |
S_IXGRP |
当前用户组的其他用户拥有执行权限 |
\ |
S_IRWXG |
当前用户组的其他用户拥有读、写、执行权限 |
其他用户 |
S_IROTH |
其他用户拥有读权限 |
\ |
S_IWOTH |
其他用户拥有写权限 |
\ |
S_IXOTH |
其他用户拥有执行权限 |
\ |
S_IRWXO |
其他用户拥有读、写、执行权限 |
2.2.1.2. close函数¶
1 2 3 4 5 6 7 8 9 10 | //所需头文件
#include <unistd.h>
// 函数原型
int close(int fd);
// 参数说明
// fd:待关闭的文件描述符
// 返回值:成功返回0,失败返回-1,并设置errno
|
打开的文件描述符必须手动close关闭,避免长期运行导致文件描述符泄漏,耗尽进程资源; 进程退出时内核会自动回收文件描述符,但手动关闭是良好的编程习惯。
2.2.2. 文件数据读写¶
2.2.2.1. read函数¶
read函数用于从指定文件描述符对应的资源中读取数据,存入用户空间缓冲区,无用户态缓存,直接从内核读取数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // 所需头文件
#include <unistd.h>
// 函数原型
ssize_t read(int fd, void *buf, size_t count);
// 参数说明
// fd:已打开的有效文件描述符
// buf:用户空间缓冲区,用于存储读取到的数据
// count:期望读取的最大字节数,建议不超过缓冲区大小
// 返回值
// >0:实际读取的字节数
// =0:已到达文件末尾(EOF)
// -1:读取失败,设置errno
|
read()返回值不一定等于count,如读取普通文件时,接近末尾会返回剩余字节数; 读取管道/套接字时,阻塞模式下会等待数据,非阻塞模式下返回-1且errno=EAGAIN。
2.2.2.2. write函数¶
write函数用于将用户空间缓冲区的数据,写入指定文件描述符对应的资源中,直接与内核交互,无用户态缓存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // 所需头文件
#include <unistd.h>
// 函数原型
ssize_t write(int fd, const void *buf, size_t count);
// 参数说明
// fd:已打开的有效文件描述符
// buf:待写入的数据缓冲区
// count:待写入的字节数
// 返回值详解
// >0:实际写入的字节数
// -1:写入失败,设置errno
|
普通文件写入时,实际写入字节数通常等于count; 磁盘空间不足、权限不足时会返回-1; 追加模式下,write会自动将数据写入文件末尾,无需手动偏移。
2.2.3. 文件偏移量控制¶
2.2.3.1. lseek函数¶
每个打开的文件都有一个对应的读写偏移量,标识下一次读写的起始位置, lseek函数用于修改文件偏移量,实现文件随机读写,类似标准I/O的fseek函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // 所需头文件
#include <sys/types.h>
#include <unistd.h>
// 函数原型
off_t lseek(int fd, off_t offset, int whence);
// 参数说明
// fd:已打开的有效文件描述符
// offset:偏移量,可正可负,正数向后偏移,负数向前偏移
// whence:偏移基准,支持三种取值:
// SEEK_SET:以文件开头为基准
// SEEK_CUR:以当前偏移量为基准
// SEEK_END:以文件末尾为基准
// 返回值详解
// 成功返回新的偏移量(相对于文件开头的字节数)
// 失败返回-1,并设置errno
|
lseek(fd, 0, SEEK_END)可获取文件大小; lseek支持“空洞文件”创建,偏移量超过文件末尾时,写入数据会生成空洞区域,不占用实际磁盘空间。
2.2.4. 文件描述符控制¶
2.2.4.1. fcntl函数¶
fcntl函数是文件描述符的“万能控制函数”,用于修改文件描述符属性、复制文件描述符、获取/设置文件状态标志等, 功能丰富,是高级系统调用I/O的常用函数。
1 2 3 4 5 6 7 8 9 10 11 12 | // 所需头文件
#include <unistd.h>
#include <fcntl.h>
// 函数原型
int fcntl(int fd, int cmd, ... /* arg */);
// 参数说明
// fd:文件描述符
// cmd:操作命令
// 返回值:成功返回值与cmd有关,失败返回-1,并且会设置errno
|
常用cmd参数:
F_DUPFD:复制文件描述符,返回新的文件描述符;
F_GETFL:获取文件状态标志,open时传入的flags;
F_SETFL:设置文件状态标志,如添加非阻塞模式;
F_GETFD:获取文件描述符标志;
F_SETFD:设置文件描述符标志。
2.2.5. 设备专用控制¶
2.2.5.1. ioctl函数¶
ioctl函数是设备驱动程序中用于管理设备I/O通道的函数。 它允许用户空间程序与内核空间的驱动模块进行交互,执行特定的控制操作,如设置串口波特率。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // 所需头文件
#include <sys/ioctl.h>
// 函数原型
int ioctl(int fd, unsigned long request, ...);
// 参数说明
// fd:文件描述符
// request:控制命令,不同的设备驱动程序会定义不同的request命令,
// 用于表示不同的操作,如读取设备状态、设置设备参数等
//...:可选参数,根据request命令的不同,可能需要传递额外的参数给驱动程序。
// 返回值:成功返回 0,失败返回-1。
|
2.3. 系统调用I/O错误处理机制¶
系统调用I/O函数失败时均返回-1,同时内核会设置全局变量errno, 存储对应的错误码,配合相关函数可快速定位错误原因,是调试底层I/O程序的关键。
2.3.1. 错误处理常用函数¶
2.3.1.1. perror函数¶
perror函数用于打印自定义提示信息+系统错误描述,直接输出至标准错误流,使用简单。
1 2 3 4 5 6 7 8 | // 所需头文件
#include <stdio.h>
// 函数原型
void perror(const char *s);
// 参数说明
// s:包含了一个自定义消息,将显示在原本的错误消息之前
|
2.3.1.2. strerror函数¶
strerror函数将errno错误码转为对应的字符串描述,便于日志记录。
1 2 3 4 5 6 7 8 9 10 | // 所需头文件
#include <string.h>
// 函数原型
char *strerror(int errnum);
// 参数说明
// errnum:错误号,通常是errno
// 返回值:返回一个指向错误字符串的指针,该错误字符串描述了错误errnum
|
2.3.2. 常见错误码与原因¶
ENOENT:文件不存在,open时路径错误;
EACCES:权限不足,无文件读写/执行权限;
EBADF:无效文件描述符,操作已关闭的fd;
EISDIR:打开的路径是目录,不可读写;
ENOSPC:磁盘空间不足,write写入失败;
EAGAIN/EWOULDBLOCK:非阻塞模式下,I/O操作暂无法完成。
2.4. 系统调用I/O示例¶
2.4.1. 文件操作实验¶
2.4.1.1. 程序源码¶
本节我们通过一个实验,来讲解如何使用对文件进行操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | #include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
//文件描述符
int fd;
char str[100];
int main(void)
{
//创建一个文件
fd = open("testscript.sh", O_RDWR|O_CREAT|O_TRUNC, S_IRWXU);
//文件描述符fd为非负整数
if(fd < 0){
perror("Fail to Open File");
return -1;
}
//写入字符串pwd
write(fd, "pwd\n", strlen("pwd\n"));
//写入字符串ls
write(fd, "ls\n", strlen("ls\n"));
//此时的文件指针位于文件的结尾处,使用lseek函数使文件指针回到文件头
lseek(fd, 0, SEEK_SET);
//从文件中读取100个字节的内容到str中,该函数会返回实际读到的字节数
read(fd, str, 100);
printf("File content:\n%s \n", str);
close(fd);
return 0;
}
|
2.4.1.2. 执行流程¶
通过open/write/read/close实现文件创建、数据写入、内容读取,演示基础系统调用流程,具体说明如下:
代码中先调用了open函数以可读写的方式打开一个文本文件,并且O_CREAT指定。 如果文件不存在,则创建一个新的文件,文件的权限为S_IRWXU,即当前用户可读可写可执行, 当前用户组和其他用户没有任何权限。
open与fopen的返回值功能类似,都是文件描述符,不过open使用非负整数来表示正常,失败时返回-1,而fopen失败时返回NULL。
创建文件后调用write函数写入了“pwd”、“ls”这样的字符串,实际上就是简单的Shell命令。
使用read函数读取内容前,先调用lseek函数重置了文件指针至文件开头处读取。 与C库文件操作的区别write和read之间不需要使用fflush确保缓冲区的内容并写入,因为系统调用的文件操作是没有缓冲区的。
最后关闭文件,释放文件描述符。
2.4.1.3. 头文件目录¶
示例代码中的开头包含了一系列Linux系统常用的头文件。今后学习Linux的过程中, 我们可能会接触各种各样的头文件,因此了解一下Linux中头文件的用法十分有必要。
在linux中,大部分的头文件在系统的“/usr/include”目录下可以找到, 它是系统自带的GCC编译器默认的头文件目录。 如果把该目录下的stdio.h文件删除掉或更改名字, 那么使用GCC编译helloworld的程序会因为找不到stdio.h文件而报错。
代码中一些头文件前包含了某个目录,比如sys/stat.h,这些头文件可以在编译器文件夹中的目录下找到。 我们通常可以使用locate命令来搜索,如:
1 2 3 4 5 6 7 8 9 10 11 | #需要安装locate软件
sudo apt install locate
#更新数据
sudo updatedb
#查找sys/stat.h
locate sys/stat.h
#信息输出如下
/usr/include/aarch64-linux-gnu/sys/stat.h
|
我们查找出sys/stat.h存在于/usr/include/aarch64-linux-gnu, 这是我们的板卡编译器头文件存放的位置之一。
2.4.1.4. 常用头文件¶
在后面的学习中我们常常会用到以下头文件,此处进行简单说明,若想查看具体的头文件内容,使用locate命令找到该文件目录后打开即可:
头文件stdio.h:C标准输入与输出(standard input & output)头文件,我们经常使用的打印函数printf函数就位于该头文件中。
头文件stdlib.h:C标准库(standard library)头文件,该文件包含了常用的malloc函数、free函数。
头文件sys/stat.h:包含了关于文件权限定义,如S_IRWXU、S_IWUSR,以 及函数fstat用于查询文件状态。涉及系统调用文件相关的操作,通常都需要用到sys/stat.h文件。
头文件unistd.h:UNIX C标准库头文件,unix,linux系列的操 作系统相关的C库,定义了unix类系统POSIX标准的符号常量头文件,比如Linux标准的输入文件描述符(STDIN),标准输出文件描述符(STDOUT),还有read、write等系统调用的声明。
头文件fcntl.h:unix标准中通用的头文件,其中包含的相关函数有 open,fcntl,close等操作。
头文件sys/types.h:包含了Unix/Linux系统的数据类型的头文件,常用的有size_t,time_t,pid_t等类型。
2.4.1.5. Makefile说明¶
Makefile是跟工程目录匹配的,本实验仅有一个C文件,且与Makefile处于同级目录。
Makefile内容如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | #定义变量
TARGET = systemcall
#定义编译器
CC = gcc
#定义头文件的位置()
CFLAGS = -I.
#定义头文件
DEPS =
#定义目标文件
OBJS = $(TARGET).o
#目标文件
$(TARGET): $(OBJS)
$(CC) -o $@ $^ $(CFLAGS)
#*.o文件的生成规则
%.o: %.c $(DEPS)
$(CC) -c -o $@ $< $(CFLAGS)
#伪目标
.PHONY: clean
#make clean清除编译结果
clean:
#删除可执行程序
rm -f $(TARGET) $(TARGET).o
#程序创建的文件
rm -f testscript.sh
|
2.4.1.6. 编译及测试¶
使用如下步骤进行编译测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | #在主机的实验代码Makefile目录下编译
make
#运行
./systemcall
#信息输出如下
File content:
pwd
ls
#程序运行后本身有输出,并且创建了一个文件
ls
#查看文件的内容
cat testscript.sh
#信息输出如下
pwd
ls
#执行生成的testscript.sh文件
./testscript.sh
#信息输出如下
/home/guest/lubancat_rk_code_storage/base_linux/file_io/systemcall
build Makefile systemcall systemcall.c testscript.sh
#设置testscript.sh权限为700,然后使用普通用户再运行systemcall
sudo chmod 700 testscript.sh
./systemcall
#信息输出如下
Fail to Open File: Permission denied
|
systemcall程序执行后,它创建的testscript.sh文件带有可执行权限,运行./testscript.sh可执行该脚本。
2.4.2. 标准输出重定向至文件实验¶
2.4.2.1. 程序源码¶
本节我们通过一个实验,来讲解如何将标准输出重定向至文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | #include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
// 打开日志文件
int log_fd = open("output.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
if (log_fd == -1) {
perror("open log file failed");
return -1;
}
// 关闭标准输出,复制log_fd至标准输出
close(STDOUT_FILENO);
if (dup(log_fd) == -1) {
perror("dup failed");
close(log_fd);
return -1;
}
// 关闭原文件描述符,仅保留重定向后的标准输出
close(log_fd);
// 以下内容会写入output.log,而非终端
printf("这行内容将写入日志文件\n");
printf("标准输出重定向成功\n");
return 0;
}
|
以上程序通过dup()复制文件描述符,将标准输出重定向至文件,实现程序输出记录到日志文件。
2.4.2.2. Makefile说明¶
Makefile与systemcall1示例的Makefile一致,此处不重复说明。
2.4.2.3. 编译及测试¶
使用如下步骤进行编译测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #在主机的实验代码Makefile目录下编译
make
#运行
./systemcall
#因为输出重定向到日志文件,所以无程序输出
#查看日志文件
cat output.log
#信息输出如下
这行内容将写入日志文件
标准输出重定向成功
|
2.4.3. 大文件复制实验¶
2.4.3.1. 程序源码¶
本节我们通过一个实验,来讲解如何进行大文件复制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | #include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define BUF_SIZE 4096 // 4KB缓冲区
int main(int argc, char *argv[]) {
// 校验参数
if (argc != 3) {
fprintf(stderr, "usage: %s <src_file> <dest_file>\n", argv[0]);
return -1;
}
// 打开源文件,只读模式
int src_fd = open(argv[1], O_RDONLY);
if (src_fd == -1) {
perror("open src file failed");
return -1;
}
// 打开目标文件,只写+创建+清空
int dest_fd = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (dest_fd == -1) {
perror("open dest file failed");
close(src_fd);
return -1;
}
// 循环读写复制文件
char buf[BUF_SIZE];
ssize_t len;
while ((len = read(src_fd, buf, BUF_SIZE)) > 0) {
if (write(dest_fd, buf, len) != len) {
perror("write dest file failed");
close(src_fd);
close(dest_fd);
return -1;
}
}
if (len == -1) {
perror("read src file failed");
close(src_fd);
close(dest_fd);
return -1;
}
// 关闭文件描述符
close(src_fd);
close(dest_fd);
printf("文件复制成功!\n");
return 0;
}
|
以上程序通过read/write实现文件复制,对比标准I/O,系统调用I/O的更高效,适合大文件拷贝场景。
2.4.3.2. Makefile说明¶
Makefile与systemcall1示例的Makefile一致,此处不重复说明。
2.4.3.3. 编译及测试¶
使用如下步骤进行编译测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | #在主机的实验代码Makefile目录下编译
make
#运行
./systemcall /etc/apt/sources.list test.txt
#信息输出如下
文件复制成功!
#查看复制后的内容
cat test.txt
#信息输出如下
deb http://mirrors.aliyun.com/ubuntu/ bionic main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ bionic-security main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ bionic-updates main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ bionic-proposed main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ bionic-backports main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ bionic main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ bionic-security main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ bionic-updates main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ bionic-proposed main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ bionic-backports main restricted universe multiverse
|
