【在Linux世界中追寻伟大的One Piece】Linux进程控制

简介: 【在Linux世界中追寻伟大的One Piece】Linux进程控制

1 -> 进程创建

1.1 -> fork函数初识

在Linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

1. #include <unistd.h>
2. 
3. pid_t fork(void);

返回值:子进程中返回0,父进程返回子进程id,出错返回-1。

进程调用fork,当控制转移到内核中的fork代码后,内核做:

  • 分配新的内存块和内核数据结构给子进程。
  • 将父进程部分数据结构内容拷贝至子进程。
  • 添加子进程到系统进程列表当中。
  • fork返回,开始调度器调度。

当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序。

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
using namespace std;
 
int main(void)
{
  pid_t pid;
 
  printf("Before: pid is %d\n", getpid());
 
  if ((pid = fork()) == -1)perror("fork()"), exit(1);
 
  printf("After:pid is %d, fork return %d\n", getpid(), pid);
 
  sleep(1);
 
  return 0;
}

运行结果:

[root@localhost linux]# ./a.out
Before: pid is 43676
After:pid is 43676, fork return 43677
After:pid is 43677, fork return 0

这里看到了三行输出,一行Before,两行After。进程43676先打印Before消息,然后它有打印After。另一个After消息有43677打印的。注意到进程43677没有打印Before,为什么呢?如下图所示 。

所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。

fork函数返回值

  • 子进程返回0。
  • 父进程返回子进程的pid。

写时拷贝

通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:

fork常规用法

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
  • 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

fork调用失败的原因

  • 系统中有太多的进程。
  • 实际用户的进程数超过了限制。

2 -> 进程终止

2.1 -> 进程退出场景

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

2.2 -> 进程常见退出方法

正常终止:

  • 从main返回。
  • 调用exit。
  • _exit。

异常退出:

  • ctrl + c,信号终止。

2.3 -> _exit函数

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

参数:status定义了进程的终止状态,父进程通过wait来获取该值。

说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行发现返回值为255。

2.4 -> exit函数

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

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

  1. 执行用户通过 atexit或on_exit定义的清理函数。
  2. 关闭所有打开的流,所有的缓存数据均被写入。
  3. 调用_exit。

实例:

#define _CRT_SECURE_NO_WARNINGS 1
 
#include <unistd.h>
#include <stdio.h>
 
void exit(int status);
 
int main()
{
  printf("hello");
 
  exit(0);
}

运行结果:

[root@localhost linux]# ./a.out

hello[root@localhost linux]#

#define _CRT_SECURE_NO_WARNINGS 1
 
#include <unistd.h>
#include <stdio.h>
 
void _exit(int status);
 
int main()
{
  printf("hello");
 
  _exit(0);
}

运行结果:

[root@localhost linux]# ./a.out

[root@localhost linux]#

2.5 -> return退出

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

3 -> 进程等待

进程等待的必要性

之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。

另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。

最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。

父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。

3.1 -> 进程等待的方法

3.1.1 -> wait方法

#include<sys/types.h>
#include<sys/wait.h>
 
pid_t wait(int* status);

返回值:成功返回被等待进程pid,失败返回-1。

参数:输出型参数,获取子进程退出状态,不关心则可以设置成NULL。

3.1.2 -> waitpid方法

#include<sys/types.h>
#include<sys/wait.h>
 
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,子进程存在且正常运行,则进程可能阻塞。

如果不存在该子进程,则立即出错返回。

3.2 -> 获取子进程status

如果传递NULL,表示不关心子进程的退出状态信息。

否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。

status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)。

#define _CRT_SECURE_NO_WARNINGS 1
 
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
 
int main(void)
{
  pid_t pid;
  if ((pid = fork()) == -1)
    perror("fork"), exit(1);
 
  if (pid == 0) 
  {
    sleep(20);
    exit(10);
  }
  else 
  {
    int st;
    int ret = wait(&st);
 
    if (ret > 0 && (st & 0X7F) == 0) 
    { // 正常退出
      printf("child exit code:%d\n", (st >> 8) & 0XFF);
    }
    else if (ret > 0) 
    { // 异常退出
      printf("sig code : %d\n", st & 0X7F);
    }
  }
}

运行结果:


[root@localhost linux]# ./a.out #等20秒退出

child exit code:10

[root@localhost linux]# ./a.out #在其他终端kill掉

sig code : 9

具体代码实现:

  • 进程的阻塞等待方式:
#define _CRT_SECURE_NO_WARNINGS 1
 
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
 
int main()
{
  pid_t pid;
  pid = fork();
  if (pid < 0) 
  {
    printf("%s fork error\n", __FUNCTION__);
 
    return 1;
  }
  else if (pid == 0) 
  { //child
    printf("child is run, pid is : %d\n", getpid());
 
    sleep(5);
 
    exit(257);
  }
  else 
  {
    int status = 0;
 
    pid_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5S
 
    printf("this is test for wait\n");
    if (WIFEXITED(status) && ret == pid) 
    {
      printf("wait child 5s success, child return code is :%d.\n", WEXITSTATUS(status));
    }
    else 
    {
      printf("wait child failed, return.\n");
 
      return 1;
    }
  }
 
  return 0;
}

运行结果:


[root@localhost linux]# ./a.out

child is run, pid is : 45110

this is test for wait

wait child 5s success, child return code is :1.


  • 进程的非阻塞等待方式:
#include <stdio.h> 
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
 
int main()
{
  pid_t pid;
  pid = fork();
  if (pid < 0) 
  {
    printf("%s fork error\n", __FUNCTION__);
 
    return 1;
  }
  else if (pid == 0) 
  { //child
    printf("child is run, pid is : %d\n", getpid());
 
    sleep(5);
 
    exit(1);
  }
  else 
  {
    int status = 0;
    pid_t ret = 0;
    do
    {
      ret = waitpid(-1, &status, WNOHANG);//非阻塞式等待
      if (ret == 0) 
      {
        printf("child is running\n");
      }
 
      sleep(1);
    } while (ret == 0);
 
    if (WIFEXITED(status) && ret == pid) 
    {
      printf("wait child 5s success, child return code is :%d.\n", WEXITSTATUS(status));
    }
    else 
    {
      printf("wait child failed, return.\n");
 
      return 1;
    }
  }
 
  return 0;
}

4 -> 进程程序替换

4.1 -> 替换原理

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

4.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函数只有出错的返回值而没有成功的返回值。

命名理解


这些函数原型看起来很容易混,但只要掌握了规律就很好记。


l(list):表示参数采用列表。

v(vector):参数用数组。

p(path):有p自动搜索环境变量PATH。

e(env):表示自己维护环境变量。

exec调用举例如下:

#include <unistd.h>
 
int main()
{
  char* const argv[] = { "ps", "-ef", NULL };
  char* const envp[] = { "PATH=/bin:/usr/bin", "TERM=console", NULL };
 
  execl("/bin/ps", "ps", "-ef", NULL);
 
  // 带p的,可以使用环境变量PATH,无需写全路径
  execlp("ps", "ps", "-ef", NULL);
 
  // 带e的,需要自己组装环境变量
  execle("ps", "ps", "-ef", NULL, envp);
  execv("/bin/ps", argv);
 
  // 带p的,可以使用环境变量PATH,无需写全路径
  execvp("ps", argv);
 
  // 带e的,需要自己组装环境变量
  execve("/bin/ps", argv, envp);
 
  exit(0);
}

事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve。这些函数之间的关系如下图所示。

下图exec函数族的一个完整的例子:

我们可以综合前面的知识,做一个简易的shell。


考虑下面这个与shell典型的互动:


[root@localhost epoll]# ls

client.cpp readme.md server.cpp utility.h

[root@localhost epoll]# ps

PID TTY TIME CMD

3451 pts/0 00:00:00 bash

3514 pts/0 00:00:00 ps

用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束。

然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序 并等待这个进程结束。


所以要写一个shell,需要循环以下过程:


获取命令行。

解析命令行。

建立一个子进程(fork)。

替换子进程(execvp)。

父进程等待子进程退出(wait)。

根据这些思路,就可以自己实现一个shell了。


实现代码:

#define _CRT_SECURE_NO_WARNINGS 1
 
#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;
}

5 -> 思考函数和进程之间的相似性

exec/exit就像call/return。


一个C程序有很多函数组成。一个函数可以调用另外一个函数,同时传递给它一些参数。被调用的函数执行一定的操作,然后返回一个值。每个函数都有他的局部变量,不同的函数通过call/return系统进行通信。这种通过参数和返回值在拥有私有数据的函数间通信的模式是结构化程序设计的基础。Linux鼓励将这种应用于程序之内的模式扩展到程序之间。如下图。

一个C程序可以fork/exec另一个程序,并传给它一些参数。这个被调用的程序执行一定的操作,然后通过exit(n)来返回值。调用它的进程可以通过wait(&ret)来获取exit的返回值。


目录
打赏
0
0
0
0
20
分享
相关文章
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
在计算机系统的底层架构中,操作系统肩负着资源管理与任务调度的重任。当我们启动各类应用程序时,其背后复杂的运作机制便悄然展开。程序,作为静态的指令集合,如何在系统中实现动态执行?本文带你一探究竟!
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
【Linux】进程概念和进程状态
本文详细介绍了Linux系统中进程的核心概念与管理机制。从进程的定义出发,阐述了其作为操作系统资源管理的基本单位的重要性,并深入解析了task_struct结构体的内容及其在进程管理中的作用。同时,文章讲解了进程的基本操作(如获取PID、查看进程信息等)、父进程与子进程的关系(重点分析fork函数)、以及进程的三种主要状态(运行、阻塞、挂起)。此外,还探讨了Linux特有的进程状态表示和孤儿进程的处理方式。通过学习这些内容,读者可以更好地理解Linux进程的运行原理并优化系统性能。
21 4
【Linux】进程IO|系统调用|open|write|文件描述符fd|封装|理解一切皆文件
本文详细介绍了Linux中的进程IO与系统调用,包括 `open`、`write`、`read`和 `close`函数及其用法,解释了文件描述符(fd)的概念,并深入探讨了Linux中的“一切皆文件”思想。这种设计极大地简化了系统编程,使得处理不同类型的IO设备变得更加一致和简单。通过本文的学习,您应该能够更好地理解和应用Linux中的进程IO操作,提高系统编程的效率和能力。
87 34
|
23天前
|
Linux:守护进程(进程组、会话和守护进程)
守护进程在 Linux 系统中扮演着重要角色,通过后台执行关键任务和服务,确保系统的稳定运行。理解进程组和会话的概念,是正确创建和管理守护进程的基础。使用现代的 `systemd` 或传统的 `init.d` 方法,可以有效地管理守护进程,提升系统的可靠性和可维护性。希望本文能帮助读者深入理解并掌握 Linux 守护进程的相关知识。
30 7
|
22天前
|
Linux 进程前台后台切换与作业控制
进程前台/后台切换及作业控制简介: 在 Shell 中,启动的程序默认为前台进程,会占用终端直到执行完毕。例如,执行 `./shella.sh` 时,终端会被占用。为避免不便,可将命令放到后台运行,如 `./shella.sh &`,此时终端命令行立即返回,可继续输入其他命令。 常用作业控制命令: - `fg %1`:将后台作业切换到前台。 - `Ctrl + Z`:暂停前台作业并放到后台。 - `bg %1`:让暂停的后台作业继续执行。 - `kill %1`:终止后台作业。 优先级调整:
37 5
Linux 进程管理基础
Linux 进程是操作系统中运行程序的实例,彼此隔离以确保安全性和稳定性。常用命令查看和管理进程:`ps` 显示当前终端会话相关进程;`ps aux` 和 `ps -ef` 显示所有进程信息;`ps -u username` 查看特定用户进程;`ps -e | grep &lt;进程名&gt;` 查找特定进程;`ps -p &lt;PID&gt;` 查看指定 PID 的进程详情。终止进程可用 `kill &lt;PID&gt;` 或 `pkill &lt;进程名&gt;`,强制终止加 `-9` 选项。
24 3
c++ linux通过实现独立进程之间的通信和传递字符串 demo
的进程间通信机制,适用于父子进程之间的数据传输。希望本文能帮助您更好地理解和应用Linux管道,提升开发效率。 在实际开发中,除了管道,还可以根据具体需求选择消息队列、共享内存、套接字等其他进程间通信方
70 16
Linux:进程间通信(共享内存详细讲解以及小项目使用和相关指令、消息队列、信号量)
通过上述讲解和代码示例,您可以理解和实现Linux系统中的进程间通信机制,包括共享内存、消息队列和信号量。这些机制在实际开发中非常重要,能够提高系统的并发处理能力和数据通信效率。希望本文能为您的学习和开发提供实用的指导和帮助。
208 20
嵌入式Linux系统编程 — 5.3 times、clock函数获取进程时间
在嵌入式Linux系统编程中,`times`和 `clock`函数是获取进程时间的两个重要工具。`times`函数提供了更详细的进程和子进程时间信息,而 `clock`函数则提供了更简单的处理器时间获取方法。根据具体需求选择合适的函数,可以更有效地进行性能分析和资源管理。通过本文的介绍,希望能帮助您更好地理解和使用这两个函数,提高嵌入式系统编程的效率和效果。
152 13
南大通用GBase 8a MPP Cluster Linux端SQL进程监控工具
南大通用GBase 8a MPP Cluster Linux端SQL进程监控工具
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等