1. 进程的基本概念
进程是操作系统中最基本的概念之一,它是正在运行的程序的实例。每个进程都有自己的内存空间、指令、数据和状态信息,它们相互独立并与其他进程隔离。进程通过操作系统的调度器进行管理,可以并发地执行多个进程。
进程可以分为以下几种类型:
1.1 前台进程和后台进程
前台进程是指用户当前正在与之交互的进程,这些进程通常会占用用户的输入/输出,用户需要等待前台进程运行完毕或被挂起后才能继续操作。后台进程则是在后台运行的进程,它们不会占用用户的输入/输出,可以同时运行多个后台进程。
在Linux系统中,可以使用以下命令将一个进程移至后台运行:
$ program_name &
其中program_name
是要移至后台运行的程序的名称,&
表示在后台运行。
1.2 父进程和子进程
每个进程都有一个父进程和零个或多个子进程。父进程是生成当前进程的进程,子进程是由父进程生成的进程。当父进程退出时,系统会自动将子进程的父进程设为init进程(进程ID为1)。
在Linux系统中,通过fork()
系统调用可以创建一个子进程:
pid_t pid = fork();
该系统调用会返回两次,一次在父进程中返回子进程的进程ID,另一次在子进程中返回0。通过判断返回值,可以在父进程和子进程中执行不同的代码。
1.3 前台进程组和后台进程组
在Linux系统中,每个进程都属于一个进程组。进程组是一组具有相同进程组ID的进程的集合。同时,系统还会为每个前台进程组和后台进程组分配一个控制终端。
前台进程组是用户当前正在与之交互的进程组,所有前台进程共享一个控制终端。后台进程组则是在后台运行的进程组,它们不会占用控制终端。
2. 进程状态
在Linux系统中,每个进程都有自己的状态,可以分为以下几种:
2.1 运行状态(Running)
运行状态表示进程正在执行中,占用CPU资源。
2.2 就绪状态(Ready)
就绪状态表示进程已经准备好,只等待系统分配CPU资源进行执行。多个就绪状态的进程会根据调度算法进行竞争,获取到CPU资源后会切换到运行状态。
2.3 等待状态(Waiting)
等待状态表示进程在等待某个事件的发生,例如等待用户输入、等待I/O操作完成等。在等待状态下,进程会暂时放弃CPU资源,直到事件发生后被重新唤醒。
等待状态又可以分为以下几种:
2.3.1 等待进程(Waiting Process)
等待进程是指当前进程正在等待某个子进程的结束,这通常发生在父进程调用wait()
系统调用时。
下面是一个使用wait()
系统调用的示例:
pid_t child_pid = fork();
if (child_pid == 0) {
// 子进程执行的代码
exit(0);
} else if (child_pid > 0) {
// 父进程执行的代码
wait(NULL); // 等待子进程结束
} else {
// fork()调用失败
}
父进程调用wait()
等待子进程结束,子进程在执行完自己的任务后调用exit()
结束自己的运行。
2.3.2 等待I/O(Waiting for I/O)
等待I/O是指进程在等待某个输入/输出操作完成,此时进程会有两个可能的状态:阻塞状态(Blocked)和睡眠状态(Sleeping)。
阻塞状态表示进程正在等待某个事件的发生,例如等待数据从磁盘读取完成。睡眠状态表示进程因为某种原因(例如等待网络连接或等待某个条件满足)而进入睡眠状态,在满足条件之前会一直等待。
等待状态的进程会被放置在等待队列中,直到事件发生或满足条件后被唤醒。
2.4 停止状态(Stopped)
停止状态表示进程被暂停执行,通常是由于收到了某个信号或调用了kill()
系统调用。处于停止状态的进程不会占用CPU资源。
2.5 僵尸状态(Zombie)
僵尸状态表示子进程已经终止,但是父进程尚未调用wait()
来获取子进程的终止状态。僵尸进程会占用系统资源,因此父进程必须调用wait()
来回收僵尸进程,释放资源。
下面是一个产生僵尸进程的示例:
pid_t child_pid = fork();
if (child_pid == 0) {
// 子进程执行的代码
exit(0);
} else if (child_pid > 0) {
// 父进程执行的代码
// 不调用wait()
} else {
// fork()调用失败
}
在上面的示例中,父进程没有调用wait()
来回收子进程,导致产生了一个僵尸进程。
要解决僵尸进程问题,可以在父进程中使用以下代码:
wait(NULL);
这样就可以回收子进程并释放资源。
3. 进程间通信
进程间通信(Inter-process Communication,IPC)是指进程之间进行信息交换和共享资源的过程,是实现进程间协作和数据交换的关键。
常用的进程间通信方式包括:
3.1 管道(Pipe)
管道是一种特殊的文件描述符,用于实现单向通信。它可以在父进程和子进程之间传递数据。
int pipefd[2];
pipe(pipefd); // 创建管道
pid_t pid = fork();
if (pid == 0) {
// 子进程执行的代码
close(pipefd[0]); // 关闭读端
write(pipefd[1], "Hello", 5); // 写入数据到管道
close(pipefd[1]); // 关闭写端
exit(0);
} else if (pid > 0) {
// 父进程执行的代码
close(pipefd[1]); // 关闭写端
char buffer[5];
read(pipefd[0], buffer, 5); // 从管道读取数据
close(pipefd[0]); // 关闭读端
printf("%s\n", buffer); // 输出数据
}
在上面的示例中,父进程创建了一个管道,并创建了一个子进程。父进程关闭了管道的写端,而子进程关闭了管道的读端。子进程通过管道的写端写入数据,父进程通过管道的读端读取数据,并输出到控制台。
3.2 命名管道(Named Pipe)
命名管道是一种具有路径名的管道,可以在不相关的进程之间进行通信。与普通管道不同,命名管道可以通过文件系统进行访问。命名管道可以通过命令mkfifo
创建。
3.3 共享内存(Shared Memory)
共享内存是一种以内存作为通信媒介的进程间通信方式。多个进程可以将某一块内存区域映射到自己的地址空间中,从而实现共享数据。共享内存通常用于高效地进行大量数据的交换。
3.4 信号(Signal)
信号是用来通知进程发生了某个事件的一种机制。当某个事件发生时,操作系统会向进程发送一个信号,进程可以选择忽略信号或处理信号,并根据信号的类型采取相应的措施。
Linux系统中的每个信号都有一个唯一的编号,可以通过kill()
系统调用向指定的进程发送信号。
下面是一个使用signal()
函数处理信号的示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
void sigint_handler(int signum) {
printf("Received SIGINT signal, exiting...\n");
exit(0);
}
int main() {
signal(SIGINT, sigint_handler); // 处理SIGINT信号
while (1) {
sleep(1);
}
return 0;
}
在上面的示例中,通过调用signal()
函数设置了一个信号处理函数sigint_handler
来处理SIGINT信号。当进程收到SIGINT信号时,会执行信号处理函数并退出。
3.5 套接字(Socket)
套接字是一种用于实现跨网络进行进程间通信的机制。套接字通常用于实现客户端-服务器模型,可以在不同的主机之间进行通信。
使用套接字进行进程间通信需要有客户端和服务器端。客户端通过套接字发送请求,服务器端接收请求并处理,并通过套接字返回结果给客户端。
// 服务器端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define PORT 8080
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
char buffer[1024] = {0};
char *hello = "Hello from server";
// 创建套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置套接字选项
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("setsockopt failed");
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 failed");
exit(EXIT_FAILURE);
}
// 接受连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept failed");
exit(EXIT_FAILURE);
}
// 接收数据
read(new_socket, buffer, 1024);
printf("Client: %s\n", buffer);
// 发送响应
send(new_socket, hello, strlen(hello), 0);
printf("Hello message sent\n");
return 0;
}
上述代码是一个简单的服务器端代码,它将监听8080端口并等待客户端的连接请求。当客户端发送请求后,服务器端会接收数据并发送响应。