Linux 系统编程

Linux 系统编程

通用 Makefile 文件

  • 通用 Makefile 文件的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SRCS := $(wildcard *.c)
Outs := $(patsubst %.c, %, $(SRCS))

CC := gcc
CFLAGS = -Wall -g

all: $(Outs)

%: %.c
$(CC) $(CFLAGS) $< -o $@

.PHONY: clean rebuild all

clean:
$(RM) $(Outs)

rebuild: clean all

目录相关操作

获取当前工作目录

  • 我们可以调用库函数 getcwd() 获取当前工作目录的绝对路径:

库函数 getcwd()

  • 如果传入的 bufNULL ,且 size0,则 getcwd() 会调用 malloc 申请合适大小的内存空间,填入当前工作目录的绝对路径,然后返回 malloc 申请的空间的地址。
    • 注意: getcwd() 不负责 free 申请的空间, free 是调用者的职责。
  • 代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
char* cwd;
if((cwd = getcwd(NULL,0)) == NULL)
{
//错误处理
perror("getcwd");
exit(1);
}
//一定处理成功
puts(cwd);
free(cwd);//由用户来free

return 0;
}
  • 也可以与库函数 error() 来联动进行错误处理,从下图中的数据手册可以看到 getcwd() 出错的时候会设置 errno 这个参数:

库函数 getcwd() 的返回值处理

  • 库函数 error() 的用法:

库函数 error() 的用法

  • 代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <unistd.h>
#include <error.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
char cwd[20];
if(getcwd(cwd,20) == NULL)
{
//错误处理
error(1, errno, "getcwd");
}
puts(cwd);

return 0;
}
  • 运行结果:

test_error 运行结果

改变当前工作目录

  • 我们可以调用库函数 chdir() 改变当前工作目录的绝对路径:

库函数 chdir()

  • 代码:
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
#include <stdio.h>
#include <unistd.h>
#include <error.h>
#include <errno.h>
#include <stdlib.h>

int main(int argc,char* argv[])
{
if(argc != 2)
{
error(1, errno, "Usage: %s path",argv[0]);
}

char* cwd;
if((cwd = getcwd(NULL,0)) == NULL)
{
error(1, errno, "getcwd");
}
puts(cwd);
free(cwd);

//改变目录的惯用法
if(chdir(argv[1]) == -1)
{
error(1, errno, "chdir %s",argv[1]);
}

if((cwd = getcwd(NULL,0)) == NULL)
{
error(1, errno, "getcwd");
}
puts(cwd);
free(cwd);

return 0;
}
  • 注意:当前工作目录是进程的属性,也就是说每一个进程都有自己的当前工作目录。且父进程创建 ( fork )子进程的时候,子进程会继承父进程的当前工作目录。
  • 运行结果:

chdir() 运行结果

创建目录

  • mkdir() 函数可以用来创建目录。

创建目录

  • 代码:
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
#include <unistd.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <stdlib.h>
#include <error.h>
#include <errno.h>

int main(int argc, char* argv[])
{
// ./test_mkdir dir mode(八进制)
//参数校验
if(argc != 3)
{
error(1, 0, "Usage %s dir mode",argv[0]);
}

//参数类型转换
mode_t mode;
sscanf(argv[2], "%o", &mode);
int err = mkdir(argv[1], mode);
if(err == -1)
{
error(1, errno, "mkdir %s",argv[1]);
}
return 0;
}
  • 当不知道 mode_t 的类型是什么的时候,可以使用以下方法来查看:
1
2
gcc -E test_mkdir.c -o test_mkdir.i
grep -nE "mode_t" test_mkdir.i
  • 运行结果:

运行结果

删除空目录

  • rmdir() 可以删除空目录。

删除空目录

  • 代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <unistd.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <stdlib.h>
#include <error.h>
#include <errno.h>

int main(int argc, char* argv[])
{
// ./test_rmdir dir
//参数校验
if(argc != 2)
{
error(1, 0, "Usage %s dir",argv[0]);
}
//功能实现
if(rmdir(argv[1]) == -1)
{
error(1, errno, "rmdir %s",argv[1]);
}

return 0;
}

目录流

  • 使用目录流,可以查看目录中的内容。
    • 流模型:“流"类似"水流”,顺序访问流中的数据时,是不需要关注位置的。
    • 目录流与文件流相比,文件流中的基本单位是字符或字节。而目录流中的基本单位是目录项。如下图所示:

目录流模型

打开目录流

  • opendir() 可以打开一个目录,得到一个指向目录流的指针 DIR*

打开一个目录

关闭目录流

  • closedir() 关闭目录流。

关闭目录流

读取目录流

  • readdir() 读目录流,得到指向下一个目录项的指针。

读取目录流

  • 结构体 dirent 的定义:

结构体 dirent 的定义

  • 读取一个目录内的内容并打印的代码:
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
#include <unistd.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <stdlib.h>
#include <error.h>
#include <errno.h>
#include <dirent.h>

int main(int argc, char* argv[])
{
// ./test_dirent dir
//参数校验
if(argc != 2)
{
error(1, 0, "Usage %s dir",argv[0]);
}
//打开目录流
DIR* stream = opendir(argv[1]);
if(!stream)
{
error(1, errno, "opendir %s",argv[1]);
}
//处理目录流
errno = 0; // 等于0表示没出错
struct dirent* pdirent;
//readdir():读目录流,得到指向下一个目录项的指针。
while((pdirent = readdir(stream)) != NULL)
{
printf("d_info=%ld, d_off=%ld, d_reclen=%hu, d_type=%d, d_name=%s\n",
pdirent->d_ino,
pdirent->d_off,
pdirent->d_reclen,
pdirent->d_type,
pdirent->d_name);
}
if(errno != 0)
{
error(1, errno, "readdir %s",argv[1]);
}
//关闭目录流
closedir(stream);

return 0;
}

递归地打印目录

  • 实现青春版 tree 命令:输出内容分为三部分
    • 1)目录的名字;
    • 2)递归打印每一个目录项;
    • 3)最后是统计信息。
  • 代码:
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
58
59
60
61
62
63
64
65
66
#include <unistd.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <stdlib.h>
#include <error.h>
#include <errno.h>
#include <dirent.h>
#include <string.h>

//width:缩进的空格数目
void dfs_print(const char* path, int width);

int directories = 0, files = 0;

int main(int argc, char* argv[])
{
// 用法: ./young_tree dir
if(argc != 2)
error(1, 0, "Usage: %s dir", argv[0]);
//遍历根节点
puts(argv[1]);
//递归打印每一个子树
dfs_print(argv[1], 4);

printf("\n%d directories, %d files\n", directories, files);

return 0;
}

void dfs_print(const char* path, int width){
//打开目录流
DIR* stream = opendir(path);
if(!stream){
error(1, errno, "opendir %s", path);
}
//遍历每一个目录项
errno = 0; // 等于0表示没出错
struct dirent* pdirent;
//readdir():读目录流,得到指向下一个目录项的指针。
while((pdirent = readdir(stream)) != NULL){
char* filename = pdirent->d_name;
//忽略 . 和 ..
if(strcmp(filename,".") == 0 || strcmp(filename,"..") == 0)
continue;
//打印这个目录项的名字
for(int i=0; i<width; i++){
putchar(' ');
}
puts(filename);
//如果是目录的话递归地处理
if(pdirent->d_type == DT_DIR){
directories++;
//拼接路径
char subpath[128];
sprintf(subpath, "%s/%s", path, filename);
dfs_print(subpath, width+4);
}
else
files++;
}
closedir(stream);
if(errno){
error(1, errno, "readdir");
}
}
  • 效果:

递归打印目录实验结果

文件相关操作

打开文件

  • 系统调用 open() 打开文件。
    • 打开成功:返回新的文件描述符(最小可用的文件描述符);
    • 打开失败:返回 -1,设置 errno

open() 系统调用的用法

  • 部分 flags 的含义:

部分 flags 的含义

  • 代码:
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
#include <unistd.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <stdlib.h>
#include <error.h>
#include <errno.h>
#include <fcntl.h>

int main(int argc, char* argv[])
{
// ./test_open file
//参数校验
if(argc != 2)
{
error(1, 0, "Usage %s file",argv[0]);
}
//打开文件,获取文件描述符
int fd = open(argv[1], O_RDWR | O_CREAT);
if(fd == -1)
{
error(1, errno, "Usage %s", argv[1]);
}
//打印获取到的文件描述符
printf("%d\n",fd);

return 0;
}
  • 通过系统调用 open() 的过程介绍内核管理文件的数据结构:
    • 文件描述符表的数组长度默认是 1024,并且可以进行设置:

文件描述符表的数组长度默认是 1024

  • 系统调用 open() 的内核管理文件流程:

open()系统调用内核管理文件流程

关闭文件

  • 系统调用 close() 关闭文件。
    • 关闭成功:返回 0
    • 关闭失败:返回 -1,设置 errno
  • 系统调用 close() 的内核管理文件流程:

close()系统调用内核管理文件流程

读取文件

  • 系统调用 read() 读取文件。
    • 读取成功:返回实际读取的字节数目(0,表示读取的起始位置在文件末尾);
    • 读取失败:返回 -1,设置 errno

系统调用read()读取文件

  • 系统调用 read() 的内核管理文件流程:

read()系统调用内核管理文件流程

写入文件

  • 系统调用 write() 读取文件。
    • 写入成功:返回实际写入字节数目(其中 n≤count );
    • 写入失败:返回 -1,设置 errno

write()系统调用

  • 系统调用 write() 的内核管理文件流程:

write()系统调用内核管理文件流程

修改文件偏移量

  • 系统调用 lseek() 修改文件偏移量,本质上就是修改 current file offset (pos) 的数值。
    • 修改成功:返回文件的位置(距离文件开头的字节数目);
    • 修改失败:返回 -1,设置 errno

系统调用 lseek() 修改文件偏移量

  • 系统调用 lseek() 的内核管理文件流程:

系统调用 lseek() 的内核管理文件流程

  • 文件库函数和文件系统调用之间的关系:

文件库函数和文件系统调用之间的关系

文件描述符和文件流的异同

  • 文件描述符的系统调用与文件流的函数:

文件描述符的系统调用与文件流的函数

  • 文件描述符和文件流的数据访问流程:

文件描述符和文件流的数据访问流程

同步内存中所有已修改的文件数据到储存设备

  • 函数 fsync() 同步内存中所有已修改的文件数据到储存设备。
    • 将和文件描述符相关联的脏页刷新到磁盘。
    • 刷新成功:返回 0
    • 刷新失败:返回 -1 并设置 errno

系统调用 fsync()

修改文件长度

  • 函数 ftruncate() 修改文件长度为 length 个字节大小。
  • 用法如下:

系统调用 ftruncate() 的用法

  • 修改文件的长度有两种情况:
    • 情况二可能会出现文件空洞的情况,此时数据全为 0 的页不会分配磁盘空间。

修改文件的长度的两种情况

  • 使用代码示例:
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
#include <unistd.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <stdlib.h>
#include <error.h>
#include <errno.h>
#include <fcntl.h>

int main(int argc, char* argv[])
{
// ./test_ftruncate file length
if(argc != 3){
error(1, 0, "Usage: %s file length", argv[0]);
}

off_t length; // 要截断的文件长度
sscanf(argv[2], "%ld", &length);

int fd = open(argv[1], O_RDWR);
if(fd == -1){
error(1, errno, "open %s", argv[1]);
}

if(ftruncate(fd, length) == -1){
error(1, errno, "ftruncate %d", fd);
} // 截断成功

close(fd);

return 0;
}
  • 关于文件空洞的测试:

关于文件空洞的测试

获取文件状态信息

  • 函数 fstat() 用于获取文件状态信息。
    • 获取成功:返回 0
    • 获取失败:返回 -1,设置 errno
  • 其具体用法和状态信息结构体如下:

fstat() 的具体用法和状态信息结构体

  • 代码示例:
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
#include <unistd.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <stdlib.h>
#include <error.h>
#include <errno.h>
#include <fcntl.h>

int main(int argc, char* argv[])
{
// ./test_fstat file
if(argc != 2){
error(1, 0, "Usage %s file", argv[0]);
}

int fd = open(argv[1], O_RDONLY);
if(fd == -1){
error(1, errno, "open %s", argv[1]);
}

struct stat sb; // 存储文件元数据信息
if(fstat(fd, &sb) == -1){
error(1, errno, "fstat %d", fd);
}

// 打印 struct stat 里面的成员信息
printf("st_ino=%ld\nst_mode=%lo\nst_nlink=%ld\nst_size=%ld\nst_blocks=%ld\n",
(long)sb.st_ino,
(long)sb.st_mode,
(long)sb.st_nlink,
(long)sb.st_size,
(long)sb.st_blocks);

return 0;
}
  • 打印结果:

打印结果

文件描述符的复制

  • 系统调用 dup() 用于对文件描述符的复制。
    • 复制成功:返回新的文件描述符;
    • 复制失败:返回 -1,设置 errno
  • 其具体用法如下,分为 dup()dup2() 两个系统调用:

两个文件描述符系统调用的具体用法

  • 系统调用 dup() 的内核管理文件流程:

系统调用 dup() 的内核管理文件流程

  • 使用 dup() 函数实现重定向:
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
#include <unistd.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <stdlib.h>
#include <error.h>
#include <errno.h>
#include <fcntl.h>

int main(int argc, char* argv[])
{
// 对 stderr 重定向
int fd = open("application.log", O_RDWR | O_CREAT | O_APPEND, 0664);
if (fd == -1){
error(1, errno, "open application.log");
}

// STDERR_FILENO 标准错误输出的文件描述符
write(STDERR_FILENO, "The first error massage\n", 24);

//对 stderr 进行重定向
close(STDERR_FILENO);
dup(fd);

write(STDERR_FILENO, "The second error massage\n", 25);

return 0;
}
  • 使用 dup2() 函数实现重定向:
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
#include <unistd.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <stdlib.h>
#include <error.h>
#include <errno.h>
#include <fcntl.h>

int main(int argc, char* argv[])
{
// 对 stderr 重定向
int fd = open("application.log", O_RDWR | O_CREAT | O_APPEND, 0664);
if (fd == -1){
error(1, errno, "open application.log");
}

// STDERR_FILENO 标准错误输出的文件描述符
write(STDERR_FILENO, "The first error massage\n", 24);

//对 stderr 进行重定向
//close(STDERR_FILENO);
if(dup2(fd, STDERR_FILENO) == -1){
error(1, errno, "dup2 %d %d", fd, STDERR_FILENO);
}

write(STDERR_FILENO, "The second error massage\n", 25);

return 0;
}
  • 分别执行完两个代码的运行结果:
    • 第一个代码重定向 STDERR_FILENO 文件描述符到 application.log 文件,写入一段语句;
    • 第二个代码同样重定向 STDERR_FILENO 文件描述符到 application.log 文件,写入一段语句。

分别执行完两个代码的运行结果

零拷贝(mmap)

  • 系统调用零拷贝( mmap() )能够省去内核态和用户态的内存拷贝。

零拷贝

  • 系统调用零拷贝( mmap() )的工作原理:
    • 相当于将文件的一部分内存通过 I/O 操作拷贝到物理内存中,并且内核态和用户态共用这部分内存,不再需要重复拷贝。

image.png

  • 系统调用零拷贝( mmap()munmap() )的使用方法:

系统调用零拷贝 mmap() 和 munmap() 的使用方法

  • 系统调用零拷贝( mmap()munmap() )的返回值:

系统调用零拷贝 mmap() 和 munmap() 的返回值

  • 系统调用零拷贝( mmap()munmap() )的复制大文件使用场景:

系统调用零拷贝 mmap() 和 munmap() 的复制大文件使用场景

  • 代码示例:
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <unistd.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <stdlib.h>
#include <error.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>

// offset 应该是页大小的整数倍
#define MMAP_SIZE (4096 * 10)

int main(int argc, char *argv[])
{
// ./mmap_cp src dst
if (argc != 3){
error(1, 0, "Usage: %s src dst", argv[0]);
}

int srcfd = open(argv[1], O_RDONLY);
if (srcfd == -1){
error(1, errno, "open %s", argv[1]);
}
// 需要读写权限
int dstfd = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0666);
if (dstfd == -1){
close(srcfd);
error(1, errno, "open %s", argv[2]);
}
// 1.需要事先知道文件大小
// 获取src大小
struct stat sb;
fstat(srcfd, &sb);
off_t fsize = sb.st_size;
// 将dst文件大小设置为fsize
ftruncate(dstfd, fsize); // 目标文件大小需要事先固定
// 设置初始偏移量,表示已经复制的数据
off_t offset = 0;
while (offset < fsize)
{
// 计算映射区长度
off_t length;
if (fsize - offset >= MMAP_SIZE)
length = MMAP_SIZE;
else
length = fsize - offset;
// 映射
void *addr1 = mmap(NULL, length, PROT_READ, MAP_SHARED, srcfd, offset);
if (addr1 == MAP_FAILED){
error(1, errno, "mmap %s", argv[1]);
}

void *addr2 = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, dstfd, offset);
if (addr2 == MAP_FAILED){
error(1, errno, "mmap %s", argv[2]);
}
// 通过将addr1的内容复制到addr2中实现文件的复制
memcpy(addr2, addr1, length);
offset += length;
// 解除映射
int err = munmap(addr1, length);
if (err == -1){
error(1, errno, "munmap %s", argv[1]);
}

err = munmap(addr2, length);
if (err == -1){
error(1, errno, "munmap %s", argv[2]);
}
} // offset == fsize
return 0;
}

CPU 的虚拟化

前置知识

  • 内核的职责:管理硬件资源
  • 共享资源的方式:
    • 时分共享(CPU)
    • 空分共享(内存)
  • 操作系统通过让一个进程运行一段时间,然后切换到其它进程,缺点是会造成性能损失(需要进行上下文切换)。
  • 如何实现 CPU 的时分共享?
    • 底层机制:如何进行上下文切换
    • 上层策略:调度策略

认识进程

  • 用户角度:进程就是正在执行的程序
  • 内核角度:要执行的任务( struct task_t
    • 进程之间必须隔离,进程之间是相互看不到的,感知不到另外的进程存在。
    • 以进程的角度看,就像它独占计算机的所有资源(抽象机制:CPU 的虚拟化)。
  • xv6 操作系统的进程相关结构体:

xv6 操作系统的进程相关结构体

底层机制:实现上下文切换的三种方式及优缺点

  • 指标:
    • 性能:不应该增加太多的系统开销
    • 控制权:操作系统应该保留控制权

方式一:直接运行(无限制)

  • 优点:简单、快
  • 缺点:没有控制权、不安全。

直接运行(无限制)

  • 如何限制应用程序的权限?
    • 不能让用户态应用程序访问非法的内存空间和执行一些特权指令。
    • 需要硬件的协助即 CPU 的模态(模式):
      • 用户态:应用程序(不能访问非法的内存空间和执行一些特权指令)
      • 内核态:操作系统(可以访问机器的所有资源)

方式二:受限直接运行协议

  • 应用程序如何执行特权操作?
    • 通过系统调用!

应用程序通过系统调用执行特权操作

  • 通过特殊指令 trap 来切换用户态和内核态:
    • 缺点在于控制权不是主动掌握在操作系统上。

受限直接运行协议

方式三:受限直接运行协议(时钟中断)

  • 方式二采用协作( yield() )的方式,等待系统调用。
  • 方式三采用非协作方式(抢占方式),操作系统能够主动进行控制。
  • 时钟中断的方式:

时钟中断的方式

  • 受限直接运行协议(时钟中断):

受限直接运行协议(时钟中断)

  • 进程之间是隔离的(感知不到内核和其他进程的存在)。
  • 进程是资源分配的最小单位(任务<-分配资源)。
  • 上下文切换:
    • 调用系统调用
    • 切换进程

和进程相关的常用命令

显示进程

  • ps 命令显示和终端关联的进程:

ps 命令显示和终端关联的进程

  • ps x 显示和用户关联的进程:

ps x 显示和用户关联的进程

  • ps aux 显示所有用户相关的进程:

ps aux 显示所有用户相关的进程

  • top 每 3 秒统计一次进程信息:

top 每 3 秒统计一次进程信息

  • pstree 打印进程树:

pstree 打印进程树

  • 前台进程:

前台进程

  • 后台进程:

后台进程

获取进程的标识

  • 获取进程的标识的用法:

获取进程的标识的用法

  • 代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
#include <unistd.h>
#include <stdio.h>

int main(int argc, char* argv[])
{
// ./test_getpid
printf("pid=%d\n",getpid());
printf("ppid=%d\n",getppid());

sleep(10);
return 0;
}
  • 运行结果:

运行结果

  • Linux 进程 id 的分配策略:

image.png

进程的基本操作

  • 创建进程:fork()
  • 终止进程:exit()_exit()abort()wait()waitpid()
  • 执行程序:exec 函数簇

创建进程:fork()

  • 系统调用 fork() 的用法:

系统调用 fork() 的用法

  • 系统调用 fork() 的返回值:

    • 成功:
      • 父进程:子进程的 pid
      • 子进程:0
    • 失败:
      • 父进程:-1,并且不会创建子进程,设置 errno
  • 惯用法代码:

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
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <error.h>
#include <errno.h>

int main(int argc, char* argv[])
{
// ./test_fork1
printf("BEGIN:\n");

//惯用法
//父子进程都是从fork()返回
//子进程不会执行前面的代码
pid_t pid = fork();

switch(pid){
case -1:
//出错
error(1, errno, "fork");
case 0:
//子进程
printf("I am a baby\n");
printf("child:pid = %d, ppid = %d\n", getpid(), getppid());
break;
default:
//父进程
printf("Who's your daddy?\n");
printf("parent:pid = %d, chilpid = %d\n", getpid(), pid);
break;
}
printf("BYE BYE!\n"); // 父子进程

return 0;
}
  • 测试结果:
    • 到底是父进程先执行还是子进程先执行是不确定的(不能假定到底是谁先执行)。

测试结果

  • 系统调用 fork() 的原理:

系统调用 fork() 的原理

代码段:父子进程共享(不能修改)
  • 栈、堆、数据段(父子进程私有)
  • 测试代码:
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
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <error.h>
#include <errno.h>

int g_value = 10; // 数据段

int main(int argc, char* argv[])
{
// ./test_fork2
int l_value = 20; // 栈
int* d_value = (int*)malloc(sizeof(int)); // 堆
*d_value = 30;

//惯用法
//父子进程都是从fork()返回
//子进程不会执行前面的代码
pid_t pid = fork();

switch(pid){
case -1:
//出错
error(1, errno, "fork");
case 0:
//子进程
g_value += 100;
l_value += 100;
*d_value += 100;
printf("g_value = %d, l_value = %d, d_value = %d\n", g_value, l_value, *d_value);
exit(0);
default:
//父进程
sleep(2);
printf("g_value = %d, l_value = %d, d_value = %d\n", g_value, l_value, *d_value);
exit(0);
}
return 0;
}
  • 测试结果:

测试结果

用户态缓冲区(文件流):父子进程是私有的

用户态缓冲区(文件流):父子进程是私有的

  • 测试代码:
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
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <error.h>
#include <errno.h>

int main(int argc, char* argv[])
{
// ./test_fork3
printf("BEGIN:"); // stdout是行缓冲区

//惯用法
//父子进程都是从fork()返回
//子进程不会执行前面的代码
pid_t pid = fork();

switch(pid){
case -1:
//出错
error(1, errno, "fork");
case 0:
//子进程
printf("I am a baby\n");
exit(0);
default:
//父进程
sleep(2);
printf("Who's your daddy?\n");
exit(0);
}
return 0;
}
  • 测试结果:

测试结果

  • 用户缓冲区父子进程私有经典题目一:
    • 总共输出 24 个 a

经典题目一

  • 用户缓冲区父子进程私有经典题目二:
    • 总共输出 14 个 a

经典题目二

打开文件(共享的)、文件描述符列表(私有的)
  • 对于父子进程,打开文件(共享的)、文件描述符列表(私有的)
  • 测试代码:
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
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <error.h>
#include <errno.h>
#include <fcntl.h>

int main(int argc, char* argv[])
{
// ./test_fork4
int fd = open("test.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
printf("origin_pos: %ld\n", lseek(fd, 0, SEEK_CUR));

//惯用法
//父子进程都是从fork()返回
//子进程不会执行前面的代码
pid_t pid = fork();
int newfd;

switch(pid){
case -1:
//出错
error(1, errno, "fork");
case 0:
//子进程
write(fd, "hello world", 11);
printf("child_pos: %ld\n", lseek(fd, 0, SEEK_CUR));
close(STDERR_FILENO);

newfd = dup(fd); // newfd = 2
printf("child_newfd = %d\n", newfd);
exit(0);
default:
//父进程
sleep(2);
printf("parent_pos: %ld\n", lseek(fd, 0, SEEK_CUR));
newfd = dup(fd); // newfd = 4
printf("parent_newfd = %d\n", newfd);
exit(0);
}
return 0;
}
  • 测试结果:

测试结果

终止进程

  • 基本概念:

终止进程的基本概念

正常终止
  • 库函数 exit() 的步骤以及系统调用 atexit() 的用法和返回值:

库函数 exit() 的步骤和返回值

  • 示例代码:
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
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <error.h>
#include <errno.h>
#include <fcntl.h>

//执行一些资源清理操作
void func(void){
printf("Something going to die...");
}

int main(int argc, char* argv[])
{
// ./test_exit
// 调用atexit()注册函数
int err = atexit(func); // 仅注册,不会执行func函数
if(err != 0){
error(1, 0, "atexit()");
}

//正常执行程序...
printf("Hello world");

exit(123); //前面分析的执行exit函数后第一步执行atexit()注册的func函数

return 0;
}
  • 测试结果:

测试结果

  • 系统调用 _exit() 的用法:
    • 退出状态码传至操作系统。

系统调用 _exit() 的用法

  • 测试程序:
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
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <error.h>
#include <errno.h>
#include <fcntl.h>

//执行一些资源清理操作
void func(void){
printf("Something going to die...");
}

int main(int argc, char* argv[])
{
// ./test_exit
// 调用atexit()注册函数
int err = atexit(func);
if(err != 0){
error(1, 0, "atexit()");
}

//正常执行程序...
printf("Hello world");

_exit(123);

return 0;
}
  • 测试结果:

测试结果

异常终止
  • 系统调用 abort() 的用法:

系统调用 abort() 的用法

  • 测试代码:
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 <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <error.h>
#include <errno.h>
#include <fcntl.h>

//执行一些资源清理操作
void func(void){
printf("Something going to die...");
}

int main(int argc, char* argv[])
{
// ./test__exit
// 调用atexit()注册函数
int err = atexit(func);
if(err != 0){
error(1, 0, "atexit()");
}

//正常执行程序...
printf("Hello world");

abort();

printf("You can't see me!!!");

return 0;
}
  • 测试结果:

测试结果

  • 内核给该进程发送 SIGABRT 信号

内核给该进程发送 SIGABRT 信号

孤儿进程和僵尸进程

  • 孤儿进程:子进程存活,父进程终止了
  • 测试代码:
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
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <error.h>
#include <errno.h>
#include <fcntl.h>

int main(int argc, char* argv[])
{
// ./orphen
pid_t pid = fork();
switch(pid){
case -1:
error(1, errno, "fork");
case 0:
//子进程
sleep(2);
printf("pid = %d, ppid = %d\n", getpid(), getppid());
exit(0);
default:
//父进程
printf("Parent: pid = %d, childpid = %d\n", getpid(), pid);
exit(0);
}
return 0;
}
  • 测试结果:

僵尸进程测试结果

  • 分析:孤儿进程会被 1 号进程(init 进程)收养,该进程一直循环执行 wait 函数。

孤儿进程分析

  • 僵尸进程:子进程死亡时,有一些信息会保存在内核(pid 、退出状态、CPU 时间…),方便父进程以后查看这个信息,并且给父进程发送 SIGCHLD 信号,但父进程默认会忽略信号。
    • 如何给僵尸进程收尸:waitwaitpid

wait()

  • 系统调用 wait() 的用法和返回值:

系统调用 wait() 的用法和返回值

  • 测试程序:
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
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <error.h>
#include <errno.h>
#include <fcntl.h>

void print_wstatus(int status){
if(WIFEXITED(status)){
int exit_code = WEXITSTATUS(status);
printf("exit_code = %d", exit_code);
}
else if(WIFSIGNALED(status)){
int signo = WTERMSIG(status);
printf("term_sig = %d", signo);
}
#ifdef WCOREDUMP
if(WCOREDUMP(status)){
printf(" (core dump) ");
}
#endif
printf("\n");
}

int main(int argc, char* argv[])
{
pid_t pid = fork();
switch(pid){
case -1:
error(1, errno, "fork");
case 0:
//子进程
printf("CHILD: pid = %d\n", getpid());
return 123;
default:
//父进程
int status; // 保存子进程的终止状态信息,位图。
pid_t childPid = wait(&status); // 阻塞点:一直等待,直到有子进程终止
if(childPid > 0){
printf("PARENT: %d terminated\n", childPid);
print_wstatus(status);
}
exit(0);
}
return 0;
}
  • 测试结果:

测试结果

waitpid()

  • 系统调用 waitpid() 的用法:

系统调用 waitpid() 的用法

  • 系统调用 waitpid() 的返回值:

系统调用 waitpid() 的返回值

  • 测试程序:
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
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <error.h>
#include <errno.h>
#include <fcntl.h>

void print_wstatus(int status){
if(WIFEXITED(status)){
int exit_code = WEXITSTATUS(status);
printf("exit_code = %d", exit_code);
}
else if(WIFSIGNALED(status)){
int signo = WTERMSIG(status);
printf("term_sig = %d", signo);
}
#ifdef WCOREDUMP
if(WCOREDUMP(status)){
printf(" (core dump) ");
}
#endif
printf("\n");
}

int main(int argc, char* argv[])
{
pid_t pid = fork();
switch(pid){
case -1:
error(1, errno, "fork");
case 0:
//子进程
printf("CHILD: pid = %d\n", getpid());
return 123;
default:
//父进程
int status; // 保存子进程的终止状态信息,位图。
pid_t childPid = waitpid(-1, &status, WNOHANG); // 阻塞点:一直等待,直到有子进程终止
if(childPid > 0){
printf("PARENT: %d terminated\n", childPid);
print_wstatus(status);
}
else if(childPid == 0){
printf("PARENT: no child changed state!\n");
}
else{
error(1, 0, "waitpid");
}
exit(0);
}
return 0;
}
  • 测试结果:

测试结果

exec 函数簇

环境变量 env 的定义:

环境变量 env 的内容

  • 打印环境变量 env 测试程序:
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
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <error.h>
#include <errno.h>
#include <fcntl.h>

int main(int argc, char* argv[])
{
// ./test_environ
// 打印进程的pid
printf("pid = %d, ppid = %d\n", getpid(), getppid());

// 打印命令行参数
for(int i=0; i<argc; i++){
puts(argv[i]);
}
printf("-----------------------------\n");
// 打印环境变量
// 声明外部变量(引用其它文件定义的environ变量)
extern char** environ;
char** cur = environ;
while(*cur){
puts(*cur);
cur++;
}
return 0;
}
  • 测试结果:

打印环境变量测试结果

exec 函数簇

  • exec 函数簇的用法:

exec 函数簇的用法

  • exec 函数簇的测试程序:
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
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <error.h>
#include <errno.h>
#include <fcntl.h>

// 定义新的环境变量
char* const new_env[] = {"USER=yugin", "TEST=0123", NULL};
// 使用execv函数簇的命令行参数向量
char* const args[] = {"./test_environ", "aaa", "bbb", "ccc", NULL};

int main(int argc, char* argv[])
{
// ./test_exce
// 打印进程的pid
printf("pid = %d, ppid = %d\n", getpid(), getppid());

printf("BEGIN:\n");

//execl("test_environ", "./test_environ", "aaa", "bbb", "ccc", NULL);
//p:会根据path环境变量查找可执行程序
//execlp("test_environ", "./test_environ", "aaa", "bbb", "ccc", NULL);
execle("test_environ", "./test_environ", "aaa", "bbb", "ccc", NULL, new_env);
//execv("test_environ", args);
//p:会根据path环境变量查找可执行程序
//execvp("test_environ", args);
//execve("test_environ", args, new_env);

printf("You cannot see here!");
error(1, errno, "exce");

return 0;
}
  • 测试结果:

exce 函数簇测试结果

  • exec 现象和原理:
    • 从上图可以看到,pidppid 没有改变,因此没有创建新的进程,并且在新的可执行程序 mian 函数的第一行开始执行。
    • 原理在于:
      • 执行 exec 函数簇会清除进程的代码段、数据段、堆、栈、上下文;
      • 加载新的可执行程序(设置代码段、数据段);
      • 从新的可执行程序 mian 函数的第一行开始执行。
  • 清除进程的代码段、数据段、堆、栈、上下文如下图所示:

清除进程的代码段、数据段、堆、栈、上下文

system 的实现和惯用法

  • system() 系统调用的用法:

system() 系统调用的用法

  • 简易 system() 的实现代码:
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
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <error.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>

int simple_system(const char* command){
pid_t childPid = fork();
int status;

switch(childPid){
case -1: // 出错
return -1;
case 0:
execl("/bin/sh", "sh", "-c", command, NULL);
_exit(127); // 出错了直接退出,避免stdout输出两次
default:
if(waitpid(childPid, &status, 0) == -1){
return -1;
}
else{
return status;
}
}
}

int main(int argc, char* argv[])
{
// ./test_system
simple_system("ls");
return 0;
}
  • 惯用法说明:

惯用法说明


Linux 系统编程
http://example.com/2025/05/18/linux_program/
作者
Mr.CHUI
发布于
2025年5月18日
许可协议