Unix/Linux进程管理
3.1 多任务处理
在计算机技术中,多任务处理指的是同时执行几个独立的任务。多任务处理是通过在不同任务之间多路复用CPU的执行时间来实现的,即将CPU执行操作从一个任务切换到另一个任务。不同任务之间的执行切换机制称为上下文切换,将一个任务的执行环境更改为另一个任务的执行环境。如果切换速度足够快,就会给人一种同时执行所有任务的错觉。这种逻辑并行性称为"并发"。
3.2 进程的概念
相关学习链接:https://blog.csdn.net/m0_65835264/article/details/127164091
操作系统是一个多任务处理系统。在操作系统中,任务也称为进程。
在实际应用中,任务和进程这两个术语可以互换使用。
执行映像定义为包含执行代码、数据和堆栈的存储区。
进程的正式定义:进程是对映像的执行。
操作系统内核将一系列执行视为使用系统资源的单一实体。
在操作系统内核中,每个进程用一个独特的数据结构表示,我们直接称它为PROC 结构体。与包含某个人所有信息的个人记录一样,PROC结构体包含某个进程的所有信息。在实际操作系统中,PROC结构体可能包含许多字段,而且数量可能很庞大。
我们来定义一个非常简单的PROC结构体来表示进程:
1 typedef struct proc{ 2 struct proc *next; //下一个进程指针 3 int *ksp; //保存sp:在字节偏移4 4 int pid; //进程ID 5 int ppid; //父进程pid 6 int status; //进程状态=PREE|READY等 7 int priority; //调度优先级 8 int kstack[1024]; //进程执行堆栈 9 } PROC;
查看进程用户:
3.7 Unix/Linux中的进程
3.7.1 进程来源
当操作系统启动时,操作系统内核的启动代码会强行创建一个PID=0的初始进程,即通过分配PROC结构体进行创建,初始化PROC内容,并让运行指向proc[0]。然后,系统执行P0,初始化系统硬件和内核数据结构。然后挂载一个根文件系统,使得系统可以使用文件。然后P0复刻出一个子进程P1,并把进程切换为用户模式运行P1。
3.7.2 INIT和守护进程
P1通常被成为INIT进程。P1开始复刻出许多子程序。P1的大部分子进程都是用来提供系统服务的,在后台运行,不与任何用户交互,成为守护进程。
守护进程例子:
syslogd:log daemon process inetd:Internet service daemon process httpd: HTTP server daemon process etc.
3.7.3 登录进程
除了守护进程外,P1还复刻出了许多LOGIN进程,每个终端一个,用于用户登录。
每个LOGIN进程打开三个与自己终端相关联的文件流。这三个文件流是用于标准输入的stdin
、用于标准输出的stdout
和用于标准错误消息的stderr
。
每个文件流都是指向进程堆区的FILE结构体指针,每个结构体记录一个文件描述符(数字)。stdin的是0,stdout是1,stderr是2。
每个LOGIN进程向stdout显示一个login:
等待用户登录。用户账户保存在/etc/passwd和/etc/shadow文件中。每个用户账户在表单的/etc/passwd文件中都有一行对应的记录:name:x:gid:uid:description:home:program
- name是用户登陆名
- x是登录检查密码
- gid是用户组ID
- uid是用户ID
- home是用户主目录
- program是用户登录后执行的初始程序
其他用户信息保存在/etc/shadow文件中。当用户尝试使用登录名和密码登录时,Linux将检查/etc/passwd文件和/etc/shadow文件(有点像PIN码这里感觉,不知道是不是),以验证用户的身份。
3.7.4 sh进程
当用户成功登陆时,LOGIN进程会获取用户的gid和uid,从而成为用户的进程。它将目录更改为用户的主目录并执行列出的程序。用户进程执行sh,故用户进程通常成为sh进程。cd、退出、注销等由sh自己执行。对于每个(可执行)文件的命令,sh会复刻一个子进程,并等待子进程终止。子进程将其执行映像更改为命令文件并执行命令程序。子进程在终止时会唤醒父进程sh。除简单的命令外,sh还支持I/O重定向和通过管道连接的多个命令。
查看sh进程:
3.7.5 进程的执行模式
进程的执行映像:
Umode进程只能通过以下三种方式进入Kmode:
- 中断
- 陷阱
- 系统调用
3.8 进程管理的系统调用
Linux中与进程管理相关的系统调用:fork(),wait(),axec(),exit()。
每个都是发出实际系统调用的库函数:int syscall (int a, int b, int c, int d)。a表示系统调用号,b,c,d表示对应核函数的参数。
3.8.1 fork()
以如下代码为例:
#include<stdio.h> int main() { int pid; printf("this is %d my parent=%d\n",getpid(),getppid()); ① pid=fork(); //复刻一个子进程 if(pid){ ② printf("this is process %d child pid=%d\n",getpid(),pid); }//打印正在执行进程的PID和新复刻子进程的PID else { ③ printf("this is process %d parent=%d\n",getpid(),getppid()); }//打印子进程的PID,应与第②行中子、父进程的PID相同 }
3.8.2 进程执行顺序
在fork()完成后,子进程与父进程和系统中所有其他进程竞争CPU运行时间。接下来运行哪个程序取决于它们的调度优先级(呈动态变化)。以如下代码演示执行顺序:
#include<stdio.h> int main() { int pid=fork(); if(pid) { printf("parent %d child=%d\n",getpid(),pid); //①sleep(1); printf("parent %d exit\n",getpid()); } else { printf("child %d start my parent=%d\n",getpid(),getppid()); //②sleep(2); printf("child %d exit my parent=%d\n",getpid(),getppid()); } }
(1)取消第一行注释,让父进程休眠1秒钟,子进程先行运行完成。
(2)取消第二行注释,但不取消第一行注释,让子进程休眠2秒钟。
(3)取消第一行和第二行的注释,结果和第二行一样。
3.8.3 进程终止
执行程序映像的进程可能以两种方式终止:
(1)正常终止。每个C程序的main函数都是由C启用代码crt0.o调用的,如果程序执行成功,main()最终会返回到crt0.o,调用库函数exit(0)来终止进程。首先,exit(value)函数会执行一些清理工作,随后,其发出一个系统调用,使进入操作系统内核的进程终止。退出0通常表示正常终止。当内核中的某个进程终止时,它会将_exit(value)系统调用中的值记录为进程PROC结构体中的退出状态。并通知其父进程并使该进程成为僵尸进程。pid=wait(int *status);
父进程可通过系统调用找到僵尸子进程,获得其pid和退出状态。它还会清空僵尸子进程PROC结构体,使该结构可被另一个进程重用。
(2)异常终止。在执行某程序时,进程可能会遇到错误,如非法指令、越权、除零等。这些错误会被CPU识别为异常。当出现异常时,它会进入操作系统内核,内核的异常处理程序将陷阱错误类型转化为一个幻数,称为信号,将信号传递给进程,使进程终止,此时僵尸进程的退出状态是信号编号。除了陷阱错误,信号也可能来自硬件或其他进程。eg:按下“Ctrl+C”组合键会产生一个硬件中断信号。或者,用户可以使用命令:kill -s signal_number pid
通过向pid识别的目标发送信号。对于大多数信号数值,进程的默认操作是终止。
在这两种情况下,当进程终止时,最终都会在操作系统内核中调用kexit()函数。唯一的区别是Unix/Linux内核将擦除终止进程的用户模式映像。
在Linux中,每个PROC都有一个2字节的退出代码(exitCode)字段,用于记录进程退出状态。如果进程正常终止,exitCode的高字节位是_exit(exitValue)系统调用中的exitValue。低字节位是导致异常终止的信号数值。因为一个进程只能死亡一次,所以只有一个字节有意义。
相关学习链接:https://blog.csdn.net/qq_43386754/article/details/85325442
3.8.4 等待子进程终止
在任何时候,一个进程都可以使用int pid=wait(int *status);
系统调用,等待僵尸子进程。如果成功,则wait()会返回僵尸子进程的PID,而且status包含僵尸子进程的exitCode。此外,wait()还会释放僵尸子进程,以供重新使用。wait()系统调用将调用内核中的kwait()函数。
以如下程序演示等待和退出系统调用:
1 #include<stdio.h> 2 #include<stdlib.h> 3 int main() 4 { 5 int pid,status; 6 pid=fork(); 7 if(pid) 8 { 9 printf("parent %d waits for child %d to die\n",getpid(),pid); 10 pid=wait(&status); 11 printf("dead child =%d,status=0x%04x\n",pid,status); 12 } 13 else{ 14 printf("child %d dies by exit(VALUE)\n",getpid()); 15 exit(100); 16 } 17 }
相关学习链接:https://blog.csdn.net/weixin_65143691/article/details/129928033
3.8.5 Linux中的subreaper进程
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <wait.h> 4 #include <sys/prctl.h> 5 int main() 6 { 7 int pid,r,status; 8 printf("mark process %d as a subreaper\n",getpid()); 9 r = prctl(PR_SET_CHILD_SUBREAPER); 10 pid = fork(); 11 if(pid) 12 { 13 printf("subreaper %d child = %d\n", getpid(), pid); 14 while (1) 15 { 16 pid = wait(&status); 17 if (pid > 0) 18 printf("subreaper %d waited a ZOMBIE=%d\n",getpid(), pid); 19 else 20 break; 21 } 22 } 23 else 24 { 25 printf("child %d parent = %d\n", getpid(), (pid_t)getppid()); 26 pid = fork(); 27 if (pid) 28 { 29 printf("child=%d start: grandchild=%d\n", getpid(),pid); 30 printf("child=%d EXIT: grandchild=%d\n",getpid(),pid); 31 } 32 else 33 { 34 printf("grandchild=%d start:myparent=%d\n",getpid(),getppid()); 35 printf("grandchild=%d EXIT:myparent=%d\n", getpid(),getppid()); 36 } 37 } 38 }
运行结果:
3.8.6 exec():更改进程执行映像
用man 3 exec查看
- exec()库函数成员
用man execve 查看execve()系统调用:
- 各参数
3.8.7 环境变量
查看环境变量:
SHELL:指定将解释任何用户命令的sh
TERM:指定运行sh时要模拟的终端类型
USER:当前登录用户
PATH:系统在查找命令时将检查的目录列表
HOME:用户的主目录。在Linux中,所有用户主目录都在/home中
HOME=/home/newhome
3.9 I/O重定向
3.9.1 文件流和文件描述符
3.9.2 文件流I/O和系统调用
scanf("%s",&item);它会试图从stdin文件输入一个(字符串)项,指向FILE结构体。如果FILE结构体的fbuff]为空,它会向Linux内核发出read系统调用,从文件描述符0中读取数据,映射到终端(/dev/ttyX)或伪终端(/dev/pts/#)键盘上。
3.9.3 重定向标准输入
3.9.4 重定向标准输出
3.10 管道
3.10.1 Unix/Linux中的管道编程
管道操作模型:
以下代码演示了管道操作:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 5 int pd[2], n,i; 6 char line[256]; 7 8 int main() 9 { 10 pipe(pd);// create a pipe 11 printf("pd=[%d, %d]\n", pd[0], pd[1]); 12 if(fork()){ 13 printf("parent %d close pd[0]\n", getpid()); 14 pclose(pd[0]);//parent as pipe WRITER 15 while(i++ < 10){ // parent writes to pipe 10 times 16 printf("parent %d writing to pipe\n", getpid()); 17 n=fwrite(pd[1],"I AM YOUR PAPA", 16); 18 printf("parent %d wrote %d bytes to pipe\n", getpid(), n); 19 } 20 printf("parent %d exit\n",getpid()); 21 } 22 else{ 23 printf("child %d close pd[1]\n", getpid()); 24 close(pd[1]);// child as Dipe READER 25 while(1){//child read from pipe 26 printf("child %d reading from pipe\n", getpid()); 27 if ((n = fread(pd[0],line,128))){ // try to read 128 bytes 28 line[n]=0; 29 printf("child read %d bytes from pipe: %s\n", n,line); 30 } 31 else // pipe has no data and no writer 32 exit(0); 33 } 34 } 35 }
3.10.2 管道命令处理
3.10.3 将管道写进程与管道读进程连接起来
3.10.4 命令管道
向ChatGpt请求苏格拉底式询问
学习总结
通过测试书上的代码案例,我对进程管理有了更加深刻的理解。对于第9、10节的内容还不是很理解,需要进一步学习相关知识。
标签:child,第三章,int,Linux,pid,Unix,getpid,printf,进程 From: https://www.cnblogs.com/20211115fyq/p/17780466.html