【Linux】--- 进程控制

什么是进程

关于进程的概念前面有文章可以回顾查看,此处不再赘述。
进程和线程详解(https://blog.csdn.net/L19002S/article/details/103498333)

Linux下如何创建一个进程

创建进程的函数原型是pid_t fork(void);
  pid_t是一个整数类型,即fork()函数会返回新进程的ID号(0~32768的整数)。
  例如:pid_t pid = fork();

注意

  • (1)新进程是当前进程的子进程。

  • (2)父进程和子进程

    • ①父进程:fork()的调用者;
    • ②子进程:新建的进程。
  • (3)子进程是父进程的复制(相同的代码,相同的数据,相同的堆栈),除了ID号和时间信息外,两者完全相同。

  • (4)子进程和父进程可以并发运行。

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int ret = fork();
printf("hello proc : %d!, ret: %d\n", getpid(), ret);
sleep(1);
return 0;
}

进程的状态及查看

1.进程状态

  • R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
  • S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))
  • D磁盘休眠状态(Disk sleep):有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
  • T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
  • X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。

2.查看进程状态命令

 ps aux / ps axj 命令

在这里插入图片描述

3.Z(zombie)-僵尸进程

僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时,就会产生僵死(尸)进程僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。

4.僵尸进程危害

  • 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,子进程就一直处于Z状态。
  • 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直需要维护。
  • 那一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费。因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
  • 最终导致一个非常严重的问题那就是内存泄漏

进程终止

1.进程退出场景

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止

2.进程常见退出方法

正常终止(可以通过 echo $? 查看进程退出码):

  1. 从main返回
  2. 调用exit
  3. _exit

异常退出
ctrl + c,信号终止

_exit函数

#include <unistd.h>
void _exit(int status);

参数:status 定义了进程的终止状态,父进程通过wait来获取该值。虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值是255。

exit函数

#include <unistd.h>
void exit(int status);

exit最后也会调用exit, 但在调用exit之前,还做了其他工作

  • 执行用户通过 atexit或on_exit定义的清理函数。
  • 关闭所有打开的流,所有的缓存数据均被写入
  • 调用_exit
    在这里插入图片描述

return退出

return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。

进程等待

1.进程等待必要性

子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

2.进程等待的方法

wait方法

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

waitpid方法

pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid:
Pid=-1,等待任一个子进程。与wait等效。
Pid>0.等待其进程ID与pid相等的子进程。
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进
程的ID。
  • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
  • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
  • 如果不存在该子进程,则立即出错返回

进程控制块(PCB)

1.进程控制块的定义

  • (1)描述进程状态、资源、与相关进程关系的数据结构;
  • (2)PCB是进程的标志。对于操作系统来说,它通过PCB来感知和管理进程;
  • (3)进程创建时会建立PCB,进程撤出时会销毁PCB。

2.PCB中的基本成员

描述进程-PCB

  • 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
  • 课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct

task_struct-PCB的一种

  • 在Linux中描述进程的结构体叫做task_struct。
  • task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。

task_ struct内容分类

  • 标示符: 描述本进程的唯一标示符,用来区别其他进程。

  • 状态: 任务状态,退出代码,退出信号等。

  • 优先级: 相对于其他进程的优先级。

  • 程序计数器: 程序中即将被执行的下一条指令的地址。

  • 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针

  • 上下文数据: 进程执行时处理器的寄存器中的数据。

  • I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。

  • 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。

  • 其他信息

3.进程的切换

(1)进程的上下文

Context,指进程运行环境,CPU环境(比如各个寄存器的取值)等等。进程的上下文由三部分组成:用户级上下文(程序、数据、共享存储区、用户栈,它们占用进程的虚拟地址空间)、寄存器上下文(由各个寄存器组成)、系统级上下文(PCB、核心栈等)。
(2)进程切换过程

  • ①进程被从堆栈调度到CPU运行;
  • ②进程被从CPU调度到堆栈暂停运行。

进程替换

1.替换原理

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

2.替换函数

其实有六种以exec开头的函数,统称exec函数:

#include <unistd.h>`
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[])

int execve(const char *path, char *const argv[], char *const envp[]);

函数解释

  • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
  • 如果调用出错则返回-1
  • 所以exec函数只有出错的返回值而没有成功的返回值。

命名理解

  1. l(list) : 表示参数采用列表
  2. v(vector) : 参数用数组
  3. p(path) : 有p自动搜索环境变量PATH
  4. e(env) : 表示自己维护环境变量

3.模拟实现shell

需要循环以下过程:

  • 获取命令行
  • 解析命令行
  • 建立一个子进程(fork)
  • 替换子进程(execvp)
  • 父进程等待子进程退出(wait)

代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#define MAX_CMD 1024
char command[MAX_CMD];
int do_face()
{
  memset(command, 0x00, MAX_CMD);
  printf("minishell$ ");
  fflush(stdout);
  if (scanf("%[^\n]%*c", command) == 0) 
  {
    getchar();
    return -1;
  }
 return 0;
}
char **do_parse(char *buff)
{
  int argc = 0;
  static char *argv[32];
  char *ptr = buff;
  while(*ptr != '\0') 
  {
   if (!isspace(*ptr)) 
   {
    argv[argc++] = ptr;
   while((!isspace(*ptr)) && (*ptr) != '\0')
    {
     ptr++;
    }
   }
  else 
  {
    while(isspace(*ptr)) 
   {
    *ptr = '\0';
    ptr++;
   }
 }
}
  argv[argc] = NULL;
  return argv;
}
int do_exec(char *buff)
{
  char **argv = {NULL};
  int pid = fork();
  if (pid == 0) 
  {
  argv = do_parse(buff);
  if (argv[0] == NULL) 
  {
    exit(-1);
  }
 execvp(argv[0], argv);
}
  else
 {
  waitpid(pid, NULL, 0);
 }
  return 0;
}
int main(int argc, char *argv[])
{
while(1) 
{
  if (do_face() < 0)
  continue;
  do_exec(command);
 }
 return 0;
}
发布了58 篇原创文章 · 获赞 301 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/L19002S/article/details/105087870