基本概念

程序和进程的区别

程序是平台相关的二进制文件,只占用磁盘空间。编写完程序代码后,编译为可执行的二进制文件即可。

进程是运行中的程序,占用 CPU、内存等系统资源。

通过 Shell 命令,可以在终端启动进程,例如执行 ls 命令:

  • 找到命令对应的二进制文件
  • 使用 fork() 函数创建新的进程
  • 在新创建的进程中调用 exec 函数组,加载命令对应的二进制文件,并从 main 函数开始执行

并发和并行

并发 concurrent:在一个时间段内,处理的请求总数。个数越多,并发越大。

并行 parallel:任意时刻能够同时处理的请求数。通常跟 CPU 内核数量相关。

Linux 进程的状态

Linux 的进程有以下 6 种状态:

  • D:深度睡眠状态,不可中断,处于这种状态的进程不能响应异步信号
  • R:进程处于运行态或就绪状态,只有在该状态的进程才可能在 CPU 上运行
  • S:可中断的睡眠状态,处于这个状态的进程因为等待某种事件的发生而被挂起
  • T:暂停状态或跟踪状态
  • X:退出状态,进程即将被销毁
  • Z:退出状态,进程成为僵尸进程

命令行控制进程

ps 命令

通过 ps aux 可以查看当前机器上的进程,其中 STAT 列的第一个字符就是进程的状态:

# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.2 190764 2140 ? Ss 2018 12:35 /usr/lib/systemd/systemd --system --deserialize 20
root 2 0.0 0.0 0 0 ? S 2018 0:00 [kthreadd]
root 3 0.0 0.0 0 0 ? S 2018 1:24 [ksoftirqd/0]

kill 命令

kill 命令用于结束进程,语法如下:

kill [-s signal|-p] [-q sigval] [-a] [--] pid...
kill -l [signal]

执行 kill 命令,系统会发送一个 SIGTERM 信号给对应的进程,请求进程正常关闭。SIGTERM 是有可能会被阻塞的。kill -9 命令用于强制杀死进程,系统给对应程序发送的信号是 SIGKILL,即 exit。exit 信号不会被系统阻塞。

示例:

kill -9 11235

PID

每个进程在创建的时候,内核都会为之分配一个全局唯一的进程号。

getpid 和 getppid 函数

通过 getpid 函数可以获取当前进程的 PID。getppid 函数可以获取父进程的 PID。

通过 ps aux 可以查看进程的 PID,资源消耗情况,通过 ps -ef 可以查看当前进程及其父进程的 PID。通过 pstree 命令可以以树状关系查看所有进程。

函数原型:

#include <sys/types.h>
#include <unistd.h> pid_t getpid(void);
pid_t getppid(void);

环境变量

除了通过 main 函数的第三个参数获取环境变量,还可以通过 environ 全局变量或 getenv() 函数来获取。

getenv 函数原型:

#include <stdlib.h>

char *getenv(const char *name);
char *secure_getenv(const char *name);

环境变量示例

#include <stdio.h>
#include <stdlib.h> extern char** environ; int main(int argc, char* argv[], char* env[])
{
int i = 0;
char* myenv = NULL;
while(env[i])
{
printf("env[%d] is: %s\n", i, env[i++]);
}
i = 0;
while(environ[i])
puts(environ[i++]); myenv = getenv("PATH");
puts(myenv);
return 0;
}

孤儿进程和僵尸进程

PCB(Process Control Block,进程控制块)是每个进程都有的数据结构,用于保存进程运行所需的信息,例如文件描述符表。

wait() 函数用来帮助父进程获取其子进程的退出状态。当进程退出时,内核为每一个进

程保存了退出状态信息。如果父进程未调用 wait() 函数,则子进程的退出信息将一直保存在内存中。

孤儿进程

Linux 中,每个进程在退出的时候,可以释放用户区空间,但是无法释放进程本身的 PCB 所占用的内存资源。PCB 必须由父进程释放。

父进程在创建子进程后退出,子进程变成孤儿进程。为防止内存泄漏,孤儿进程被 init 进程领养,init 进程变成孤儿进程的父进程。

下面示例中,父进程先退出:

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h> int main()
{
pid_t pid = fork(); if (pid == 0)
{
printf("child pid is: %d, ppid is: %d\n", getpid(), getppid());
sleep(1);
printf("child pid is: %d, ppid is: %d\n", getpid(), getppid());
}
else if (pid > 0)
{
sleep(0.5);
printf("parent pid is: %d, ppid is: %d\n", getpid(), getppid());
printf("parent exit\n");
}
return 0;
}

输出:

parent pid is: 3348, ppid is: 713
parent exit
child pid is: 3349, ppid is: 1
child pid is: 3349, ppid is: 1

僵尸进程

子进程退出了,父进程一直未调用 wait 或 waitpid 函数,子进程就变成了僵尸进程。

代码控制进程

进程入口函数 main

可执行的二进制文件,都是从 main 函数开始执行的。main 函数有 3 种原型定义:

int main();
int main(int argc, char *argv[]);
int main(int argc, char *argv[], char *env[]);

参数:

  • argc:参数个数
  • argv:参数数组,每个参数都是字符串
  • env:环境变量数组,每个环境变量都是字符串

进程基本操作

注意,Shell 终端无法检测进程是否创建了子进程。在进程执行完毕后,Shell 会立即回到交互状态,此时如果子进程还在输出数据,会打印在 Shell 的命令提示符之后。可以在父进程中 sleep 一下。

创建进程

Linux 中用 fork() 函数创建新进程,函数原型如下:

#include<unistd.h>

pid_t fork(void);

返回值:

成功创建进程时,会对父子进程各返回一次,对父进程返回子进程的 PID,对子进程返回 0。通过条件分支语句可以分别进行不同处理。失败则返回小于 1 的错误码。

fork 函数执行的时候,会将当前正在运行的进程完整的复制一份,提供给子进程。子进程从 fork 函数之后开始执行

创建多个子进程

通过 for 循环,可以创建多个子进程,只需要在每次 fork 之后判断如果是子进程则结束循环即可。通过循环下标可以判断当前子进程是第几次创建的:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h> int main()
{
int i = 0;
int number = 5;
pid_t pid; for (i = 0; i < number; i++)
{
pid = fork();
if (pid == 0) break;
} // 子进程
if (i == 0) printf("first process, pid = %d\n", getpid());
if (i == 1) printf("second process, pid = %d\n", getpid());
//... // 父进程
if (i == number) printf("parent process, pid = %d\n", getpid());
return 0;
}

终止进程

进程的终止分为两种:

  • 正常终止:

    • 从 main() 函数中 return 返回,实际上也是调用的 exit() 函数
    • 调用 exit() 类函数
  • 异常终止:
    • 调用 abort() 函数
    • 接收到终止信号

exit 函数原型如下:

#include <stdlib.h>

void exit(int status);

exit(0) 等价于 return 0

exec 函数组

fork 函数可以复制一份父进程,得到的子进程跟父进程有完全一样的代码跟数据。之后两个进程各自执行,互不影响。

实际上我们通常需要子进程执行不同的代码,这时就需要通过 exec 函数加载代码段,并跳转到新代码段的 main 入口执行。

函数原型:

#include <unistd.h>

extern char **environ;

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 execvpe(const char *file, char *const argv[], char *const envp[]);

exec 函数组是在 exec 上加 l、v、p、e 四个后缀形成的,这些函数作用相同,只是在参数列表上存在差别。

  • 后缀 p:用文件名做参数,如果文件名中不含路径则会去 PATH 环境变量所指定的各个目录中搜索可执行文件。无后缀 p 则使用绝对路径名来指定可执行文件的位置。
  • 后缀 e:表示可以传递一个指向环境字符串指针数组的指针,环境数组需要以 NULL 结束。而无此后缀的函数则使用调用进程中 environ 变量为新程序复制现有的环境。
  • 后缀 l:用 list 形式来传递新程序的参数,传给新程序的所有参数以可变参数的形式传递,最后一个参数必须是 NULL。
  • 后缀 v:用 vector 形式来传递新程序的参数,传给新程序的所有参数放入一个字

    符串数组中,数组以 NULL 结束。

返回值:exec 族函数报错时才有返回值 -1,否则无返回值。如果执行到后面的代码,就是出错了。

示例:

char* myenv[] = {"TEST=666", "HOME=/home/kika", NULL};
execle("home/kika/test", "hello", "world", myenv);

完整示例:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h> int main()
{
int pid = fork();
if (pid > 0) {
exit(0);
} else if (pid == 0) {
execle("home/kika/test", "hello", "world", myenv);
perror("execle error");
exit(-1);
} else {
perror("fork error");
}
return -1;
}

wait 和 waitpid 函数

通常,在父进程中调用 wait 函数,可以查看子进程的退出信息,让子进程撤单结束。wait 函数是阻塞式的,waitpid 可以设置为非阻塞的。父进程根据创建的子进程个数,在循环中通过 wait 函数逐个回收子进程。而 waitpid 函数则可以通过 PID 等待指定的子进程退出。

wait 函数调用一次只会回收一个子进程。多个子进程需要调用多次 wait。

函数原型:

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

wait 参数:

  • status:进程退出的原因。可以通过预定义的宏来检查:

    • WIFEXITED(status):若为正常终止子进程返回状态,则为真
    • WEXITSTATUS(status):获取子进程传给 exit 或 return 的参数
    • WIFSIGNALED(status):若为异常终止子进程返回状态,则为真
    • WTERMSIG(status):获取使子进程退出的信号

waitpid 参数:

  • pid:用于设置 waitpid 工作方式。

    • -1:等价于 wait,等待任意子进程退出
    • 0:等待组 ID 等于调用进程的组 ID 的任一子进程退出
    • > 0:等待 PID 等于该数值的进程退出
    • < -1:等待其组 ID 等于该数值的任一子进程退出
  • options:用于设置 waitpid 是否阻塞执行。0 则阻塞,设为 WNOHANG 则非阻塞。

返回值:成功时返回退出子进程的 PID,没有子进程时返回 -1.

综合示例

创建三个子进程,分别运行自定义程序,shell 程序,未定义程序(段错误)。然后在父进程中通过 wait 回收所有子进程,并分别判断退出原因:

#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <errno.h> int main(int argc, char* argv[])
{
int num = 3;
int i;
pid_t pid;
for (i = 0; i < 3; i++) {
pid = fork();
if (pid == 0) break;
} if (i == 0) {
execlp("ps", "ps", "aux", NULL);
perror("execlp ps error");
exit(1);
} else if (i == 1) {
execl("/root/test/api/process/myls", "", NULL);
perror("execl myls error");
exit(1);
} else if (i == 2) {
execl("./error", "", NULL);
perror("execl ./error");
exit(1);
} else {
int status;
pid_t pid;
while (pid = wait(&status) != -1) {
printf("children PID is: %d\n", pid);
if (WIFEXITED(status)) {
printf("return value is: %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("died by signal: %d\n", WTERMSIG(status));
}
}
}
return 0;
}

【Linux 应用编程】进程管理 - 进程、线程和程序的更多相关文章

  1. linux高级编程基础系列:线程间通信

    linux高级编程基础系列:线程间通信 转载:原文地址http://blog.163.com/jimking_2010/blog/static/1716015352013102510748824/ 线 ...

  2. linux c编程:初识进程与线程

    p { margin-bottom: 0.25cm; line-height: 120% } (一) 认识进程 在Linux系统中,每一个进程都有自己的ID,就如同人的身份证一样.linux中有一个数 ...

  3. Linux:使用systemd管理进程

    Blog:博客园 个人 概述 systemd是目前Linux系统上主要的系统守护进程管理工具,由于init一方面对于进程的管理是串行化的,容易出现阻塞情况,另一方面init也仅仅是执行启动脚本,并不能 ...

  4. Linux网络编程学习(二) ----- 进程控制(第三章)

    1.进程和程序 程序是一个可执行文件,而一个进程是一个执行中的程序实例.一个进程对应于一个程序的执行,进程是动态的,程序是静态的,多个进程可以并发执行同一个程序.比如几个用户可以同时运行一个编辑程序, ...

  5. linux:查看以及管理进程

    学习笔记内容概要 进程查看的命令:top,ps,pstree 进程管理的命令:kill,nice,renice 查看进程: 一.top工具 top 工具是我们常用的一个查看工具,能实时的查看我们系统的 ...

  6. Linux多线程编程,为什么要使用线程,使用线程的理由和优点等

    线程?为什么有了进程还需要线程呢,他们有什么区别?使用线程有什么优势呢?还有多线程编程的一些细节问题,(http://www.0830120.com)如线程之间怎样同步.互斥,这些东西将在本文中介绍. ...

  7. 进程管理—进程描述符(task_struct)

    http://blog.csdn.net/qq_26768741/article/details/54348586 当把一个程序加载到内存当中,此时,这个时候就有了进程,关于进程,有一个相关的叫做进程 ...

  8. 一、linux IO 编程---内存管理

    1.1 进程在虚拟空间中的布局 32位的操作系统虚拟空间的大小为 4GB,即每个进程在系统中分配的虚拟空间大小为4GB.这4GB的大小被分为了两个部分: 内核空间:1GB,内核起的进程 用户空间:3G ...

  9. linux网络编程之简单的线程池实现

    转眼间离15年的春节越来越近了,还有两周的工作时间貌似心已经不在异乡了,期待与家人团聚的日子,当然最后两周也得坚持站好最后一班岗,另外期待的日子往往是心里不能平静的,越是想着过年,反而日子过得越慢,于 ...

随机推荐

  1. 理解BurpSuit Intruder几种攻击方式

    Intruder标签下有四种攻击方式 Sniper Battering Ram Pitchfork Cluster Bomb 假设用户名密码词典分别如下: user1,user2,usre3 pass ...

  2. 【转】rsa公钥和私钥的生成

    转:https://www.cnblogs.com/zengsf/p/10136886.html 在liunx环境中 openssl 然后生成私钥: genrsa -out app_private_k ...

  3. init.uniform / unit.normal

    均匀分布nn.init.uniform(tensor,a=0,b=1)tensor -n维的torch.Tensora 均匀分布的下界,默认值为0b 均匀分布的上界,默认值为1 正态分布torcn.n ...

  4. 【洛谷P3413】萌数

    题目大意:求区间 [l,r] 内萌数的个数,其中萌数定义为数位中存在长度至少为 2 的回文子串的数字. 题解:l, r 都是 1000 位级别的数字,显然是一道数位 dp 的题目,暴力直接去世. 发现 ...

  5. Idea创建多模块依赖Maven项目

    idea 创建多模块依赖Maven项目   本来网上的教程还算多,但是本着自己有的才是自己的原则,还是自己写一份的好,虽然可能自己也不会真的用得着. 1. 创建一个新maven项目 2. 3. 输入g ...

  6. 设计模式来替代if-else

    前言# 物流行业中,通常会涉及到EDI报文(XML格式文件)传输和回执接收,每发送一份EDI报文,后续都会收到与之关联的回执(标识该数据在第三方系统中的流转状态).这里枚举几种回执类型:MT1101. ...

  7. 谁掳走了 nginx.pid 文件?

    1.重载配置 执行 nginx  -s   reload  命令,报错:找不到 nginx.pid 文件,无法打开.曾经屡试不爽的命令,此时,竟然失灵了? 刚开始,我一头雾水,有点丈二和尚摸不着头脑… ...

  8. 介绍HTML5几种存储方式

    总体情况 h5之前,存储主要是用cookies.cookies缺点有在请求头上带着数据,大小是4k之内.主Domain污染. 主要应用:购物车.客户登录 对于IE浏览器有UserData,大小是64k ...

  9. BZOJ 4769: 超级贞鱼 逆序对 + 归并排序

    手画几下序列的变换后发现逆序对数是恒定的,故只需对第 $0$ 年求逆序对即可. 树状数组会 $TLE$ 的很惨,需要用到归并排序来求逆序对. 其实就是省掉了一个离散化的时间,估计能比树状数组快一半的时 ...

  10. GAN one-shot

    基于one-shot的GAN生成图片 GAN的学习资料用于数据增广GAN的调研: https://zhuanlan.zhihu.com/p/32103958 GAN的各种paper汇集(包括Gener ...