什么是系统调用

系统调用是操作系统提供对外提供服务的方式,应用程序通过系统调用请求操作系统完成特定功能

在C语言中,系统调用表现为一系列函数的调用,常见的系统调用函数如下

进程控制

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

int main() {
pid_t pid;

// 创建新进程
pid = fork();

if (pid < 0) {
// fork失败
perror("fork failed");
return 1;
} else if (pid == 0) {
// 子进程
printf("This is the child process. PID: %d\n", getpid());
printf("Child process ID: %d\n", getpid());
} else {
// 父进程
printf("This is the parent process. PID: %d\n", getpid());
printf("Parent process ID: %d\n", getpid());
printf("Created child process with PID: %d\n", pid);
}

return 0;
}

fork系统调用能让程序拥有多个执行流,提供了在程序中实现多任务的能力

fork后的父进程和子进程虽然拥有独立的内存空间(包括堆内存,使用写时复制(COW)技术实现),但它们共享某些操作系统级别的资源,如底层文件描述符、网络套接字,例如如下情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//网络编程中,服务器程序接受一个客户端连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
//fork子进程处理客户端连接
pid_t pid = fork();
if (pid == 0) {
// 子进程
close(server_fd); // 子进程关闭监听套接字
handle_client(new_socket); // 处理客户端请求
close(new_socket); // 处理完请求后关闭套接字
exit(0);
} else if (pid > 0) {
// 父进程
close(new_socket); // 父进程关闭连接套接字,继续接受新的连接
}

fork后,socket本身并没有被克隆,只是它的引用被复制了,所以为避免互相干扰,各进程关闭多余的socket引用复制,只留自身需要的引用,避免出现并发安全问题。

  • **exec()**:执行一个程序(有多个变种,如execl()、execp()等)。

子程序

1
2
3
4
5
6
7
8
9
10
11
12
// child.c
#include <stdio.h>

int main(int argc, char **argv) {
printf("Hello from the child program!\n");
printf("Received parameters: ");
for(int i=0;i<argc;i++){
printf("%s ",argv[i]);
}
printf("\n");
return 0;
}

父程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// parent.c
#include <stdio.h>
#include <unistd.h>

int main() {
printf("This is the parent program.\n");
printf("Now executing the child program...\n");

// 使用execl()执行child程序
execl("./child", "child", NULL);

// 如果exec成功,以下代码不会执行
printf("If you see this message, exec failed!\n");
return 0;
}

execl(): 这是exec系列调用的一种,参数包括程序路径、参数列表,参数列表以NULL结尾。

"./child": 指定要执行的可执行文件(child)。

"child": 传递给子程序的第一个参数,通常是程序名(argv[0])。

Shell(如bash、zsh等)使用exec系统调用来执行用户输入的命令。例如,当你在命令行中输入一个命令时,Shell解析命令后,会使用fork()创建一个子进程,然后在子进程中调用exec来执行该命令对应的程序。exec会将当前进程的内容替换为新程序,而不会创建新的进程,这节省了系统资源。

  • **exit()**:终止进程并返回状态码。
  • **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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
pid_t pid = fork();

if (pid < 0) {
// fork()失败
perror("Fork failed");
return 1;
} else if (pid == 0) {
// 子进程
printf("Child process (PID: %d) is running...\n", getpid());
sleep(2); // 模拟子进程的工作
printf("Child process (PID: %d) is exiting...\n", getpid());
exit(42); // 子进程终止,返回状态码42
} else {
// 父进程
int status;
printf("Parent process (PID: %d) is waiting for child process (PID: %d) to finish...\n", getpid(), pid);
wait(&status); // 等待子进程结束

if (WIFEXITED(status)) {
printf("Child process terminated normally with exit status %d\n", WEXITSTATUS(status));
} else {
printf("Child process terminated abnormally\n");
}
}

return 0;
}

应用场景:一个父进程创建多个子进程,每个子进程负责处理一个独立的任务。父进程使用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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
pid_t pids[3];
int i;

// 创建三个子进程
for (i = 0; i < 3; i++) {
pids[i] = fork();

if (pids[i] < 0) {
// fork()失败
perror("Fork failed");
return 1;
} else if (pids[i] == 0) {
// 子进程
printf("Child %d (PID: %d) is running task...\n", i + 1, getpid());
sleep(2 + i); // 每个子进程休眠不同的时间
printf("Child %d (PID: %d) is exiting...\n", i + 1, getpid());
exit(10 + i); // 返回不同的状态码(10, 11, 12)
}
}

// 父进程等待所有子进程结束
for (i = 0; i < 3; i++) {
int status;
pid_t pid = wait(&status); // 等待任意一个子进程结束

if (pid > 0) {
printf("Parent process collected child process (PID: %d)\n", pid);
if (WIFEXITED(status)) {
printf("Child process (PID: %d) terminated with exit status %d\n", pid, WEXITSTATUS(status));
} else {
printf("Child process (PID: %d) terminated abnormally\n", pid);
}
} else {
perror("Wait failed");
}
}

printf("All child processes have finished. Parent process (PID: %d) is exiting...\n", getpid());
return 0;
}

wait()系统调用会使父进程阻塞,直到它的任意一个子进程终止。wait()返回的值是结束的子进程的进程ID

  • **getpid()**:获取当前进程ID。
  • **getppid()**:获取父进程ID。
  • **kill()**:向进程发送信号。

文件操作

  • **open()**:打开文件。
  • **read()**:从文件中读取数据。

函数原型

1
ssize_t read(int fd, void *buf, size_t count);

参数

**fd**:要读取的文件描述符。这个文件描述符通常是由 open 函数返回的,或者是标准输入、输出和错误流(如 stdin、stdout、stderr)的文件描述符。

**buf**:指向一个内存缓冲区的指针,用于存储读取的数据。

**count**:要读取的最大字节数。

返回值

成功:返回实际读取的字节数。如果返回值小于 count,则表示读取的字节数少于请求的字节数,可能是因为到达了文件末尾或缓冲区已满。

失败:返回 -1,并设置 errno 以指示错误类型。常见的错误包括 EBADF(文件描述符无效)、EFAULT(缓冲区指针无效)和 EINTR(操作被中断)。

例子:从标准输入读取数据

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 <stdio.h>
#include <unistd.h>

#define BUF_SIZE 3

int main() {
char buffer[BUF_SIZE];
ssize_t bytesRead;

while(1){
// 从标准输入读取数据
bytesRead = read(STDIN_FILENO, buffer, sizeof(buffer) - 1);

// 检查读取是否成功
if (bytesRead < 0) {
perror("read");
return 1;
}

// 添加字符串终止符
buffer[bytesRead] = '\0';

// 打印读取的数据
printf("Read %zd bytes: %s\n", bytesRead, buffer);
}
return 0;
}

read函数会阻塞直到有可获得的数据

  • **write()**:向文件中写入数据。
  • **close()**:关闭文件描述符。
  • **lseek()**:移动文件指针。
  • **unlink()**:删除文件。
  • **stat()**:获取文件状态信息。
  • **fstat()**:获取文件描述符的状态信息。
  • **lstat()**:获取文件状态信息(处理符号链接)。

文件输出

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 <stdio.h>
#include <fcntl.h> // open, O_WRONLY, O_CREAT, O_TRUNC
#include <unistd.h> // write, close

int main() {
int fd;
char *filename = "example.txt";
char write_buf[] = "Hello, Linux system calls!";
ssize_t bytes_written;

// 打开文件进行写操作,如果文件不存在则创建文件
fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
perror("open for write");
return 1;
}

// 将数据写入文件
bytes_written = write(fd, write_buf, sizeof(write_buf));
if (bytes_written < 0) {
perror("write");
close(fd);
return 1;
}

printf("Data written to %s\n", filename);

// 关闭文件
close(fd);

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
34
35
36
#include <stdio.h>
#include <fcntl.h> // open, O_RDONLY
#include <unistd.h> // read, close

int main() {
int fd;
char *filename = "example.txt";
char read_buf[256];
ssize_t bytes_read;

// 打开文件进行读操作
fd = open(filename, O_RDONLY);
if (fd < 0) {
perror("open for read");
return 1;
}

// 读取文件中的数据
bytes_read = read(fd, read_buf, sizeof(read_buf) - 1);
if (bytes_read < 0) {
perror("read");
close(fd);
return 1;
}

// 确保字符串以空字符结尾
read_buf[bytes_read] = '\0';

// 打印读取的数据
printf("Read from file: %s\n", read_buf);

// 关闭文件
close(fd);

return 0;
}

3. 内存管理

  • **mmap()**:映射文件到内存。
  • **munmap()**:解除映射。
  • **brk()**:调整数据段大小(用于堆)。
  • **sbrk()**:增加或减少堆的大小。

4. 设备操作

  • **ioctl()**:控制设备行为。

ioctl是input output control的缩写,是一个非常强大的系统调用函数,能做很多需要root权限的操作

例子:获取网络接口的ip地址

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <net/if.h>
#include <arpa/inet.h>
#include <unistd.h>

int main() {
int sockfd;
struct ifreq ifr;
struct sockaddr_in* ipaddr;

// 创建一个套接字,SOCK_DGRAM 表示数据报套接字,虽然实际操作不涉及数据传输,但这是一个必要的步骤
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket");
exit(EXIT_FAILURE);
}

// 设置接口名称,IFNAMSIZ 是一个常量,定义了网络接口名称的最大长度.它在 <net/if.h> 头文件中定义
strncpy(ifr.ifr_name, "eth0", IFNAMSIZ - 1);

// 获取IP地址
ifr.ifr_addr.sa_family = AF_INET;
//SIOCGIFADDR 的全称是 "Socket IOCTL Get Interface Address"。它是 ioctl 系统调用中的一个命令,用于获取网络接口的 IP 地址
if (ioctl(sockfd, SIOCGIFADDR, &ifr) < 0) {
perror("ioctl");
close(sockfd);
exit(EXIT_FAILURE);
}

// 将结果转换为 sockaddr_in 结构
ipaddr = (struct sockaddr_in *)&ifr.ifr_addr;

//inet_ntoa函数用于将 IPv4 地址从网络字节顺序的 in_addr 结构转换为点分十进制格式的字符串。
printf("IP Address: %s\n", inet_ntoa(ipaddr->sin_addr));

// 关闭套接字
close(sockfd);
return 0;
}

网络接口相关的 ioctl 命令

  1. **SIOCGIFNAME**:获取接口名称。
  2. **SIOCGIFBRDADDR**:获取广播地址。
  3. **SIOCGIFNETMASK**:获取网络掩码。
  4. **SIOCGIFHWADDR**:获取硬件地址(MAC 地址)。
  5. **SIOCGIFFLAGS**:获取接口标志(如是否启用)。
  6. **SIOCSIFFLAGS**:设置接口标志(如启用或禁用接口)。
  7. **SIOCGIFMTU**:获取接口的最大传输单元(MTU)。
  8. **SIOCSIFMTU**:设置接口的最大传输单元(MTU)。

重磅级ioctl命令**TUNSETIFF**(TUN SET Interface 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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <net/if.h>
#include <unistd.h>
#include <fcntl.h>
#include <linux/if_tun.h>

int main() {
int fd;
struct ifreq ifr;
char *dev = "/dev/net/tun";

// 打开 TUN 设备 O_RDWR表示读写模式
fd = open(dev, O_RDWR);
if (fd < 0) {
perror("open");
exit(EXIT_FAILURE);
}

// 清空 ifreq 结构体
memset(&ifr, 0, sizeof(ifr));

// 设置设备名称
ifr.ifr_flags = IFF_TUN | IFF_NO_PI; // 使用 TUN 设备,不添加协议头
strncpy(ifr.ifr_name, "tun0", IFNAMSIZ);

// 创建 TUN 设备, 当程序结束时,tun设备也会消失
if (ioctl(fd, TUNSETIFF, (void *)&ifr) < 0) {
perror("ioctl TUNSETIFF");
close(fd);
exit(EXIT_FAILURE);
}

printf("Created TUN device: %s\n", ifr.ifr_name);

//暂停10秒,此时可以用ip addr命令查看生成的tun0接口
sleep(10);

// 关闭 TUN 设备
close(fd);
return 0;
}

5. 网络通信

  • **socket()**:创建一个套接字。
  • **bind()**:绑定套接字到一个地址。
  • **listen()**:监听套接字上的连接请求。
  • **accept()**:接受连接请求。
  • **connect()**:连接到一个远程主机。
  • **send()**:发送数据。
  • **recv()**:接收数据。
  • **sendto()**:发送数据到指定地址。
  • **recvfrom()**:从指定地址接收数据。
  • **shutdown()**:关闭套接字的某些功能。
  • **getsockname()**:获取套接字的本地地址。
  • **getpeername()**:获取套接字的对端地址。

一个简单的回显服务器和客户端程序

服务器程序

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
75
76
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
char *welcome_message = "Welcome to the echo server!\n";

// 创建套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}

// 设定套接字选项
int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}

// 配置服务器地址结构
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);

// 绑定套接字
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}

// 监听连接
if (listen(server_fd, 3) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}

printf("Server is listening on port %d...\n", PORT);

while (1) {
// 接受连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}

// 发送欢迎消息
send(new_socket, welcome_message, strlen(welcome_message), 0);

while (1) {
// 读取客户端数据
int bytes_read = read(new_socket, buffer, BUFFER_SIZE);
if (bytes_read <= 0) {
// 客户端关闭连接
printf("Client disconnected.\n");
close(new_socket);
break;
}
// 回显数据
send(new_socket, buffer, bytes_read, 0);
memset(buffer, 0, BUFFER_SIZE);
}
}

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
int sock = 0;
struct sockaddr_in serv_addr;
char buffer[BUFFER_SIZE] = {0};
char *message = "Hello, Server!";

// 创建套接字
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("Socket creation error");
exit(EXIT_FAILURE);
}

// 配置服务器地址结构
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);

// 转换IPv4地址到网络字节序
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
perror("Invalid address/ Address not supported");
exit(EXIT_FAILURE);
}

// 连接到服务器
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("Connection failed");
exit(EXIT_FAILURE);
}

// 发送消息
send(sock, message, strlen(message), 0);
printf("Message sent: %s\n", message);

// 读取服务器回显数据
int bytes_read = read(sock, buffer, BUFFER_SIZE);
printf("Message from server: %s\n", buffer);

close(sock);
return 0;
}

6. 进程间通信

  • **pipe()**:创建管道。
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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main() {
int pipefd[2];
pid_t pid;
char buf[100];
const char* message = "Hello from the parent process!";

// 创建管道
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}

// 创建子进程
pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}

if (pid == 0) { // 子进程
close(pipefd[1]); // 关闭写端

// 从管道中读取数据
read(pipefd[0], buf, sizeof(buf));
printf("Child process received: %s\n", buf);

close(pipefd[0]);
exit(EXIT_SUCCESS);
} else { // 父进程
close(pipefd[0]); // 关闭读端

// 向管道中写入数据
write(pipefd[1], message, strlen(message) + 1);

close(pipefd[1]); // 关闭写端
wait(NULL); // 等待子进程结束
exit(EXIT_SUCCESS);
}
}

管道适用于父子进程间的通信

  • **mkfifo()**:创建FIFO(命名管道)。

写进程

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 <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
const char* fifo_path = "/tmp/myfifo";
const char* message = "Hello from the FIFO writer!";
int fd;

// 创建FIFO
if (mkfifo(fifo_path, 0666) == -1) {
perror("mkfifo");
exit(EXIT_FAILURE);
}

// 打开FIFO进行写操作
fd = open(fifo_path, O_WRONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}

// 向FIFO中写入数据
write(fd, message, strlen(message) + 1);

close(fd);
unlink(fifo_path); // 删除FIFO
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
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
const char* fifo_path = "/tmp/myfifo";
char buf[100];
int fd;

// 打开FIFO进行读操作
fd = open(fifo_path, O_RDONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}

// 从FIFO中读取数据
read(fd, buf, sizeof(buf));
printf("Received message: %s\n", buf);

close(fd);
return 0;
}

FIFO(命名管道)允许不相关的进程进行通信

7. 文件系统操作

  • **mount()**:挂载文件系统。
  • **umount()**:卸载文件系统。

8. 权限管理

  • **chmod()**:更改文件权限。
  • **chown()**:更改文件所有者。