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资源,文件描述符就是指向表中对应项的下标。

文件描述符分配规则如下:

  1. 文件描述符从0开始递增分配,遵循“最小未使用”原则,内核会优先分配当前最小的空闲整数;

  2. 每个进程默认打开3个标准文件描述符,由系统自动分配,无需手动打开/关闭;

  3. 进程关闭文件描述符后,对应整数会被回收,可重新分配给新打开的资源;

  4. 单个进程可打开的文件描述符数量有限制,可通过系统配置修改。

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. 程序源码

本节我们通过一个实验,来讲解如何使用对文件进行操作。

文件操作实验(base_linux/file_io/systemcall1/systemcall.c文件)
 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内容如下所示:

文件操作实验(base_linux/file_io/systemcall1/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. 程序源码

本节我们通过一个实验,来讲解如何将标准输出重定向至文件。

标准输出重定向至文件实验(base_linux/file_io/systemcall2/systemcall.c文件)
 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. 程序源码

本节我们通过一个实验,来讲解如何进行大文件复制。

大文件复制实验(base_linux/file_io/systemcall3/systemcall.c文件)
 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