第七周Linux教材第三章学习笔记——Unix/Linux进程管理

发布时间 2023-10-22 15:05:26作者: 20211115fyq

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节的内容还不是很理解,需要进一步学习相关知识。