本帖最后由 dirty 于 2025-1-1 16:16 编辑
本篇讲述进程相关知识点。
Linux 内核把进程称为任务(task),进程的虚拟地址空间分为用户虚拟地址空间和内核虚拟地址空间,所有进程共享内核虚拟地址空间,每个进程有独立的用户虚拟地址空间。进程有两种特殊形式:没有用户虚拟地址空间的进程称为内核线程,共享用户虚拟地址空间的进程称为用户线程,通常在不会引起混淆的情况下把用户线程简称为线程。共享同一个用户虚拟地址空间的所有用户线程组成一个线程组。
进程指正在运行的程序, 如下图示, 是资源分配的最小单位, 可以通过“ps ” 或“top” 等命令查看正在运行的进程, 线程是系统的最小调度单位, 一个进程可以拥有多个线程, 同一进程里的线程可以共享此进程的同一资源。
每个进程都有一个唯一的标识符, 既进程 ID, 简称 pid。进程间的通信的几种方法:
① 管道通信: 有名管道, 无名管道
②信号通信: 信号的发送, 信号的接受, 信号的处理
③ IPC 通信: 共享内存, 消息队列, 信号灯
④ Socket 通信
进程的三种基本状态以及转换:
创建进程
在 Linux内核中,新进程是从一个已经存在的进程复制出来的。内核使用静态数据构造出0号内核线程,0号内核线程分叉生成1号内核线程和2号内核线程(kthreadd线程)。1号内核线程完成初始化以后装载用户程序,变成1号进程,其他进程都是1号进程或者它的子孙进程分叉生成的;其他内核线程是 kthreadd 线程分叉生成的。
三个系统调用可以用来创建新的进程。
(1)fork(分叉):子进程是父进程的一个副本,采用了写时复制的技术。
(2)vfork:用于创建子进程,之后子进程立即调用execve 以装载新程序的情况。为了避免复制物理页,父进程会睡眠等待子进程装载新程序。现在fork采用了写时复制的技术,vfork 失去了速度优势,已经被废弃。
(3)clone(克隆):可以精确地控制子进程和父进程共享哪些资源。这个系统调用的主要用处是可供pthread库用来创建线程。
clone是功能最齐全的函数,参数多,使用复杂,fork是clone的简化函数。fork是比较常见用法。
在内核底层文件kernel/fork.c、arch/arm64/kernel/process.c、kernel/sched/core.c等内核实现创建新进程及其实现、唤醒进程、装载进程。
下面在父进程创建子进程
#include <stdio.h>
#include <unistd.h>
int main(void)
{
pid_t pid;
pid = fork();
if (pid < 0)
{
printf("fork is error \n");
return -1;
}
//父进程
if (pid > 0)
{
printf("This is parent,parent pid is %d\n", getpid());
}
//子进程
if (pid == 0)
{
printf("This is child,child pid is %d,parent pid is %d\n", getpid(), get
ppid());
}
return 0;
}
装载 ELF 程序
ELF文件:ELF(Executable and Linkable Format)是可执行与可链接格式,主要有以下4种类型。
●目标文件(object fle),也称为可重定位文件(relocatable fle),扩展名是“.o”,多个目标文件可以链接生成可执行文件或者共享库。
●可执行文件(executable fle)。0
●共享库(sharedobject fle),扩展名是“.so”。
●核心转储文件(core dump fle)。
ELF 文件分成4个部分:ELF首部、程序首部表(program header table)节(section)和节首部表(sectionheader table)。只有ELF 首部的位置是固定的,其余各部分的位置和大小由 ELF首部的成员决定。ELF文件格式如下图所示组成:
进程退出
进程退出分两种情况:进程主动退出和终止进程。exit用来使一个线程退出。Linux私有的系统调用exit_group用来使一个线程组的所有线程退出。
终止进程是通过给进程发送信号实现的,kil用来发送信号给进程或者进程组。tkill用来发送信号给线程,参数tid是线程标识符。tgkill用来发送信号给线程,参数tgid 是线程组标识符,参数tid是线程标识符。
进程调度
Linux内核支持的调度策略如下,调度进程的核心函数是schedule()。
●限期进程使用限期调度策略(SCHED DEADLINE)。
●实时进程支持两种调度策略:先进先出调度(SCHED_FIFO)和轮流调度(SCHED_RR)。
●普通进程支持两种调度策略:标准轮流分时(SCHED_NORMAL)和空闲(SCHEDIDLE)。
SMP调度
SMP调度(Symmetric Multi-Processing调度)是指在对称多处理器(SMP)系统中,操作系统如何管理和调度多个处理器上的进程,以确保高效和公平的资源利用。在SMP系统中,进程调度器必须支持以下特性,
●需要使每个处理器的负载尽可能均衡。
●可以设置进程的处理器亲和性(affinity),即允许进程在哪些处理器上执行。
●可以把进程从一个处理器迁移到另一个处理器。
发散与应用:
在我们实际应用中,经常会使用到进程间通讯。Linux 进程间通信机制分三类: 数据交互, 同步, 信号。
无名管道是最古老的进程通信方式, 有如下两个特点:
●只能用于有关联的进程间数据交互, 如父子进程, 兄弟进程, 子孙进程, 在目录中看不到文件节点, 读写文件描述符存在一个 int 型数组中。
●只能单向传输数据, 即管道创建好后, 一个进程只能进行读操作, 另一个进程只能进行写操作,读出来字节顺序和写入的顺序一样。
无名管道使用步骤:
1. 调用 pipe()创建无名管道;
2.fork()创建子进程, 一个进程读, 使用 read(), 一个进程写, 使用 write()
使用示例代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
int main(void)
{
char buf[32] = {0};
pid_t pid;
// 定义一个变量来保存文件描述符
// 因为一个读端, 一个写端, 所以数量为 2 个
int fd[2];
// 创建无名管道
pipe(fd);
printf("fd[0] is %d\n", fd[0]);
printf("fd[2] is %d\n", fd[1]);
// 创建进程
pid = fork();
if (pid < 0)
{
printf("error\n");
}
if(pid > 0)
{
int status;
close(fd[0]);
write(fd[1], "hello", 5);
close(fd[1]);
wait(&status);
exit(0);
}
if (pid == 0)
{
close(fd[1]);
read(fd[0], buf, 32);
printf("buf is %s\n", buf);
close(fd[0]);
exit(0);
}
return 0;
}
有名管道在一些专业书籍中叫做命名管道, 它的特点是
1.可以使无关联的进程通过 fifo 文件描述符进行数据传递;
2.单向传输有一个写入端和一个读出端, 操作方式和无名管道相同。
有名管道使用步骤:
1. 使用 mkfifo()创建 fifo 文件描述符。
2. 打开管道文件描述符。
3. 通过读写文件描述符进行单向数据传输。
有名管道代码示例
/* fifo_write.c */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
int ret;
char buf[32] = {0};
int fd;
if (argc < 2)
{
printf("Usage:%s <fifo name> \n", argv[0]);
return -1;
}
if (access(argv[1], F_OK) == 1)
{
ret = mkfifo(argv[1], 0666);
if (ret == -1)
{
printf("mkfifo is error \n");
return -2;
}
printf("mkfifo is ok \n");
}
fd = open(argv[1], O_WRONLY);
while (1)
{
sleep(1);
write(fd, "hello", 5);
}
close(fd);
return 0;
}
/* fifo_read.c */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main(int argc, char *argv[])
{
char buf[32] = {0};
int fd;
if (argc < 2)
{
printf("Usage:%s <fifo name> \n", argv[0]);
return -1;
}
fd = open(argv[1], O_RDONLY);
while (1)
{
sleep(1);
read(fd, buf, 32);
printf("buf is %s\n", buf);
memset(buf, 0, sizeof(buf));
}
close(fd);
return 0;
}
信号是 Linux 系统响应某些条件而产生的一个事件, 接收到该信号的进程会执行相应的操作。
信号发送
信号的产生有三种方式:
1)由硬件产生, 如从键盘输入 Ctrl+C 可以终止当前进程
2)由其他进程发送, 如可在 shell 进程下, 使用命令 kill -信号标号 PID, 向指定进程发送信号。
3)异常, 进程异常时会发送信号
信号发送函数有:kill、raise、alarm,具体API可查阅资料使用
接收信号
如果要让我们接收信号的进程可以接收到信号, 那么这个进程就不能停止。 让进程不停止有三种方法:
1) while
2) sleep
3) pause
信号处理
信号有三种处理方式:
1.默认方式(通常是终止进程) ,
2.忽略, 不进行任何操作。
3.捕捉并处理调用信号处理器(回调函数形式) 。
这里第三种处理方式示例代码如下:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void myfun(int sig)
{
if(sig == SIGINT)
{
printf("get sigint\n");
}
}
int main(void)
{
signal(SIGINT,myfun);
while(1){
sleep(1);
printf("wait signal\n");
}
return 0;
}
此外还有共享内存、消息队列、信号量,这里再讲讲消息队列。消息队列是类 unix 系统中一种数据传输的机制, 其他操作系统中也实现了这种机制, 可见这种通信机制在操作系统中有重要地位。Linux 内核为每个消息队列对象维护一个 msqid_ds, 每个 msqid_ds 对应一个 id, 消息以链表形式存储,并且 msqid_ds 存放着这个链表的信息。
消息队列的特点:
1.发出的消息以链表形式存储, 相当于一个列表, 进程可以根据 id 向对应的“列表” 增加和获取消息。
2.进程接收数据时可以按照类型从队列中获取数据。
消息队列的使用步骤:
1. 创建 key;
2. msgget()通过 key 创建(或打开) 消息队列对象 id;
3. 使用 msgsnd()/msgrcv()进行收发;
4. 通过 msgctl()删除 ipc 对象
/* a.c 向消息队列里面写 */
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
struct msgbuf
{
long mtype;
char mtext[128];
};
int main(void)
{
int msgid;
key_t key;
struct msgbuf msg;
//获取 key 值
key = ftok("./a.c", 'a');
//获取到 id 后即可使用消息队列访问 IPC 对象
msgid = msgget(key, 0666 | IPC_CREAT);
if (msgid < 0)
{
printf("msgget is error\n");
return -1;
}
printf("msgget is ok and msgid is %d \n", msgid);
msg.mtype = 1;
strncpy(msg.mtext, "hello", 5);
//发送数据
msgsnd(msgid, &msg, strlen(msg.mtext), 0);
return 0;
}
/* b.c 从消息队列里面读 */
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
struct msgbuf
{
long mtype;
char mtext[128];
};
int main(void)
{
int msgid;
key_t key;
struct msgbuf msg;
key = ftok("./a.c", 'a');
//获取到 id 后即可使用消息队列访问 IPC 对象
msgid = msgget(key, 0666 | IPC_CREAT);
if (msgid < 0)
{
printf("msgget is error\n");
return -1;
}
printf("msgget is ok and msgid is %d \n", msgid);
//接收数据
msgrcv(msgid, (void *)&msg, 128, 0, 0);
printf("msg.mtype is %ld \n", msg.mtype);
printf("msg.mtext is %s \n", msg.mtext);
return 0;
}
本篇对Linux内核进程管理有了一些认识,根据以往经验拓展发散讲了些进程方面的使用。学以致用,可以拿适合的开发板及其配套资料实践使用,会有不一样的收获感。
|