进程

发布时间 2023-03-31 23:04:41作者: ⭐⭐-fighting⭐⭐

问1. 什么是程序,什么是进程,有什么区别?

程序是静态的概念,gcc xxx.c –o pro,磁盘中生成pro文件,叫做程序

进程是程序的一次运行活动,通俗点意思是程序跑起来了,系统中就多了一个进程

问2. 如何查看系统中有哪些进程?

a.使用ps指令查看

实际工作中,配合grep来查找程序中是否存在某一个进程

ps -aux|grep init

b.使用top指令查看,类似windows任务管理器

问3. 什么是进程标识符?

每个进程都有一个非负整数表示的唯一ID,叫做pid,类似身份证

Pid=0: 称为交换进程(swapper)作用---进程调度

Pid=1:init进程 作用---系统初始化

编程调用getpid函数获取自身的进程标识符

getppid获取父进程的进程标识符

问4. 什么叫父进程,什么叫子进程

进程A创建了进程B

那么A叫做父进程,B叫做子进程,父子进程是相对的概念,理解为人类中的父子关系

问5. C程序的存储空间是如何分配?

https://blog.csdn.net/FHNCSDN/article/details/108690687

·正文段。这是由CPU执行的机器指令部分。通常,正文段是可共享的,所以即使是频繁
执行的程序(如文本编辑器、C编译器和shll等)在存储器中也只需有一个副本,另外,
正文段常常是只读的,以防止程序由于意外而修改其自身的指令。
·初始化数据段。通常将此段称为数据段,它包含了程序中需明确地赋初值的变量。例如,
C程序中出现在任何函数之外的声明:
int maxcount = 99;
使此变量带有其初值存放在初始化数据段中。
·非初始化数据段。通常将此段称为ss段,这一名称来源于一个早期的汇编运算符,意思
是“block started by symbol”(由符号开始的块),在程序开始执行之前,内核将此段中的
数据初始化为0或空指针。出现在任何函数外的C声明
long sum[1000]:
使此变量存放在非初始化数据段中。
·栈。自动变量以及每次函数调用时所需保存的信息都存放在此段中。每次调用函数时,
其返回地址以及调用者的环境信息(例如某些机器寄存器的值)都存放在栈中。然后,
最近被调用的函数在栈上为其自动和临时变量分配存储空间。通过以这种方式使用栈,
可以递归调用C函数。递归函数每次调用自身时,就使用一个新的栈帧,因此一个函数调
用实例中的变量集不会影响另一个函数调用实例中的变量。
·堆。通常在堆中进行动态存储分配。由于历史上形成的惯例,堆位于非初始化数据段和
栈之间。

进程间通信

进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。

IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。

1. 管道(无名管道)

管道,通常指无名管道,是 UNIX 系统IPC最古老的形式。

特点:

  1. 它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。
  2. 它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。
  3. 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。

原型:

1 #include <unistd.h>
2 int pipe(int fd[2]);    // 返回值:若成功返回0,失败返回-1

当一个管道建立时,它会创建两个文件描述符:fd[0]为读而打开,fd[1]为写而打开。如下图:

img

要关闭管道只需将这两个文件描述符关闭即可。

例子

单个进程中的管道几乎没有任何用处。所以,通常调用 pipe 的进程接着调用 fork,这样就创建了父进程与子进程之间的 IPC 通道。如下图所示:

323808-20160311094030069-935122142

若要数据流从父进程流向子进程,则关闭父进程的读端(fd[0])与子进程的写端(fd[1]);反之,则可以使数据流从子进程流向父进程。

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main()
{
	int fd[2];
	int pid;
	char buf[128];

//	int pipe(int pipefd[2]);

	if(pipe(fd) == -1){
		printf("creat pipe failed\n");
	}
	pid = fork();
	
	if(pid<0){
		printf("creat child failed\n");
	}
	else if(pid > 0){
		sleep(3);
		printf("this is father\n");
		close(fd[0]);
		write(fd[1],"hello from father",strlen("hello form father"));
		wait();
	}else{
		printf("this is child\n");
		close(fd[1]);
		read(fd[0],buf,128);
		printf("read from father: %s\n",buf);
		exit(0);
	}
	return 0;
}

fork()编程实战 参考博客

一个现有进程 可以调用fork函数创建一个新进程。

#include <unistd.h>

Pid_t fork(void);

​ 返回值:子进程中返回0,父进程中返回子进程ID,出错返回-1

由fork创建的新进程被称为子进程。fork函数被调用一次,但返回两次。两次返回的唯一区别是子进程的返回值是0,而父进程的返回值则是新子进程的进程ID。将子进程ID返回给父进程的理由是:因为一个进程的子进程可以有多个,并且没有一个函数使一个进程可以获得其所有子进程的进程ID。fork使子进程得到返回值0的理由是:一个进程只会有一个父进程,所以子进程总是可以调用getpid以获得其父进程的进程ID(进程ID 0总是由内核交换进程使用,所以一个子进程的进程ID不可能为0)。

子进程和父进程继续执行fork调用之后的指令。子进程是父进程的副本。例如,子进程获得父进程数据空间、堆和栈的副本。注意,这是子进程所拥有的副本。父、子进程并不共享这些存储空间部分。父、子进程共享正文段。

由于在fork之后经常跟随着exec,所以现在的很多实现并不执行一个父进程、栈和堆的完全复制。作为替代,使用了写时复制(Copy-On-Write,COW)技术。这些区域由父、子进程共享,而且内核将它们的访问权限改变为只读的。如果父、子进程中的任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储器系统中的一“页”。

  • pid_t fork(void);//pid_t为int类型,进行了重载
  • pid_t getpid();// 获取当前进程的 pid 值。
  • pid_t getppid(); //获取当前进程的父进程 pid 值

fork()特性

fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:

在父进程中,fork返回新创建子进程的进程ID;
在子进程中,fork返回0;
如果出现错误,fork返回一个负值;
因此我们可以通过fork返回的值来判断当前进程是子进程还是父进程。(注: fork 调用生成的新进程与其父进程谁先执行不一定,哪个进程先执行要看系统的进程调度策略)

fork创建一个子进程的目的:

1、一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程

中是常见的——父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求到达。

2、一个进程要执行一个不同的程序。这对shell是常见的情况。在这种情况下,子进程从fork返回后立即调用exec。

一个现有进程 可以调用fork函数创建一个新进程。

#include <unistd.h>

Pid_t fork(void);

​ 返回值:子进程中返回0,父进程中返回子进程ID,出错返回-1

由fork创建的新进程被称为子进程。fork函数被调用一次,但返回两次。两次返回的唯一区别是子进程的返回值是0,而父进程的返回值则是新子进程的进程ID。将子进程ID返回给父进程的理由是:因为一个进程的子进程可以有多个,并且没有一个函数使一个进程可以获得其所有子进程的进程ID。fork使子进程得到返回值0的理由是:一个进程只会有一个父进程,所以子进程总是可以调用getpid以获得其父进程的进程ID(进程ID 0总是由内核交换进程使用,所以一个子进程的进程ID不可能为0)。

子进程和父进程继续执行fork调用之后的指令。子进程是父进程的副本。例如,子进程获得父进程数据空间、堆和栈的副本。注意,这是子进程所拥有的副本。父、子进程并不共享这些存储空间部分。父、子进程共享正文段。

由于在fork之后经常跟随着exec,所以现在的很多实现并不执行一个父进程、栈和堆的完全复制。作为替代,使用了写时复制(Copy-On-Write,COW)技术。这些区域由父、子进程共享,而且内核将它们的访问权限改变为只读的。如果父、子进程中的任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储器系统中的一“页”。

vfork函数

也可以创建进程,与fork的区别:

1、vfork直接使用父进程的存储空间,不拷贝

2、vfork保证子进程先运行,当子进程调用exit退出后,父进程才执行

进程退出

正常退出

1.Main函数调用return

2.进程调用exit(),标准c库

3.进程调用_exit()或者_Exit(),属于系统调用

补充:

1.进程最后一个线程返回

2.最后一个线程调用pthread_exit

异常退出

1.调用abort

2.当进程收到某些信号时,如ctrl+C

3.最后一个线程对取消(cancellation)请求做出响应

不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打
开描述符,释放它所使用的存储器等。
对上述任意一种终止情形、我们都希望终止进程能够通知其父进程它是如何终止的。对于
三个终止函数(exit、_exit和Exit),实现这一点的方法是,将其退出状态(exit status)
作为参数传送给函数。在异常终止情况下,内核(不是进程本身)产生一个指示其异常终止原
因的终止状态(termination status)。在任意一种情况下,该终止进程的父进程都能用wait或
waitpid函数(在下一节说明)取得其终止状态。

image-20230331220011746

image-20230331220018956

等待子进程

为啥要等待子进程退出

父进程等待子进程退出,并收集子进程的退出状态

检查walt和wa1tpld所返回的终止状态的宏
说明
WIFEXITED(status) 若为正常终止子进程返回的状态,则为真。对于这种情况可执行WEXITSTATUS
(status),取子进程传送给exit、_exit或Exit参数的低8位
WIFSIGNALED (status) 若为异常终止子进程返回的状态,则为真(接到一个不捕捉的信号)。对于这
种情况,可执行WTERMSIG(status),取使子进程终止的信号编号。另外,有些
实现(非Single UNIX Specification)定义宏WCOREDUMP(status),若已产生终
止进程的core文件,则它返回真
WIFSTOPPED (status) 若为当前暂停子进程的返回的状态,则为真。对于这种情况,可执行WSTOPSIG
(status),取使子进程暂停的信号编号
WIFCONTINUED (status) 若在作业控制暂停后已经继续的子进程返回了状态,则为真。(POSIX.1的XSI
的扩展,仅用于waitpid)

子进程退出的几种状态:

  • 子进程退出状态不被收集,变成僵死进程(僵尸进程)
  • 父进程如果不等待子进程退出,在子进程之前就结束了自己的“生命”,此时子进程叫做孤儿进程。Linux避免系统存在过多孤儿进程,init进程收留孤儿进程,变成孤儿进程的父进程

相关函数

image-20230331222300303

pid参数的作用解释如下:

pid == -1
等待任 一 子进程。就这一方面而言,waitpid与wait等效。
pid>0
等待其进程ID与pid相等的子进程。
pid==0
等待其组D等于调用进程组D的任一子进程。(9.4节将说明进程组。)
pid <-1
等待其组D等于pid绝对值的任一子进程。

status参数:是一个整型数指针

非空:子进程退出状态放在它所指向的地址中。

空:不关心退出状态

waitpid的options常量
常量 说明
WCONTINUED 若实现支特作业控制.那么由d指定的任一子进程在暂停后已经继续,但其状态尚未报告,
则返回其状态(POSIX.1的XSI扩展)
WNOHANG 若由pid指定的子进程并不是立即可用的,则waitpid不阻塞,此时其返同值为0
WUNTRACED 若某实现支特作业控制,而由d指定的任一子进程已处于暂停状态,并且其状态自暂停以
来还未报告过,则返同其状态。WIFSTOPPED宏确定返回值是否对应干一个暂停子进程
  • 如果其所有子进程都还在运行,则阻塞。
  • 如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。
  • 如果它没有任何子进程,则立即出错返回。

区别:

wait使调用者阻塞,waitpid有一个选项,可以使调用者不阻塞

检查wait和waitpid所返回的终止状态的宏
说明
WIFEXITED (status) 若为正常终止子进程返回的状志,则为真。对于这种情况可执行WEXITSTATUS
(stauts),取子进程传送给exit、_exit 或 _Exit参数的低8位
WIFSIGNALED (status) 若为异常终止子进程返回的状态,则为真(接到一个不捕捉的信号)。对于这
种情况,可执行WTERMSIG(status),取使子进程终止的信号编号。另外,有些
实现(非Single UNIX Specification)定义宏WCOREDUMP(status),若已产生终
止进程的core文件,则它返回真
WIFSTOPPED (status) 若为当前暂停子进程的返回的状态,则为真。对于这种情况,可执行WSTOPSIG
(status),取使子进程暂停的信号编号
WIFCONTINUED(status) 若在作业控制暂停后已经继续的子进程返回了状态,则为真。(POSIX.I的XSI
扩展,仅用于waitpid。)

exec族函数 参考博文

为什么要用exec族函数,有什么作用

(1)一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程
中是常见的一父进程等待客户端的服务请求。当这种请求到达时,父进程调用Eok,使子
进程处理此请求。父进程则继续等待下一个服务请求到达。
(2)一个进程要执行一个不同的程序。这对shll是常见的情况。在这种情况下,子进程从
fork返回后立即调用exec(我们将在8.l0节说明exec)。

exec配合fork使用

实现功能,当父进程检测到输入为1的时候,创建子进程把配置文件的字段值修改掉。

2. FIFO(命名管道)

FIFO,也称为命名管道,它是一种文件类型。

  1. FIFO可以在无关的进程之间交换数据,与无名管道不同。
  2. FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。

mkfifo

1 #include <sys/stat.h>
2 // 返回值:成功返回0,出错返回-1
3 int mkfifo(const char *pathname, mode_t mode);

其中的 mode 参数与open函数中的 mode 相同。一旦创建了一个 FIFO,就可以用一般的文件I/O函数操作它。

当 open 一个FIFO时,是否设置非阻塞标志(O_NONBLOCK)的区别:

  • 若没有指定O_NONBLOCK(默认),只读 open 要阻塞到某个其他进程为写而打开此 FIFO。类似的,只写 open 要阻塞到某个其他进程为读而打开它。
  • 若指定了O_NONBLOCK,则只读 open 立即返回。而只写 open 将出错返回 -1 如果没有进程已经为读而打开该 FIFO,其errno置ENXIO。

函数说明

mkfifo()会依参数pathname建立特殊的FIFO文件,该文件必须不存在,而参数mode为该文件的权限(mode%~umask),因此 umask值也会影响到FIFO文件的权限。Mkfifo()建立的FIFO文件其他进程都可以用读写一般文件的方式存取。当使用open()来打开 FIFO文件时,O_NONBLOCK旗标会有影响
1、当使用O_NONBLOCK 旗标时,打开FIFO 文件来读取的操作会立刻返回,但是若还没有其他进程打开FIFO 文件来读取,则写入的操作会返回ENXIO 错误代码。
2、没有使用O_NONBLOCK 旗标时,打开FIFO 来读取的操作会等到其他进程打开FIFO文件来写入才正常返回。同样地,打开FIFO文件来写入的操作会等到其他进程打开FIFO 文件来读取后才正常返回。
返回值
若成功则返回0,否则返回-1,错误原因存于errno中。
错误代码
EACCESS 参数pathname所指定的目录路径无可执行的权限
EEXIST 参数pathname所指定的文件已存在。
ENAMETOOLONG 参数pathname的路径名称太长。
ENOENT 参数pathname包含的目录不存在
ENOSPC 文件系统的剩余空间不足
ENOTDIR 参数pathname路径中的目录存在但却非真正的目录。
EROFS 参数pathname指定的文件存在于只读文件系统内。

#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include<errno.h>
#include <fcntl.h>
//open 要阻塞到某个其他进程为读而打开它
//       int mkfifo(const char *pathname, mode_t mode);
int main()
{
	if( (mkfifo("./file",0600) == -1) && errno!=EEXIST){
		printf("mkfifo failuer\n");
		perror("why");
	}
	int fd = open("./file",O_RDONLY);
	printf("open success\n");
	return 0;
}
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include<errno.h>
#include <fcntl.h>
//
//       int mkfifo(const char *pathname, mode_t mode);
int main()
{
	int fd = open("./file",O_WRONLY);
	printf("write open success\n");
	return 0;
}
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
//read
int main() {
    char buf[30] = {0};
    int nread = 0;

    if ((mkfifo("./file", 0600) == -1) && errno != EEXIST) {
        printf("mkfifo failuer\n");
        perror("why");
    }

    int fd = open("./file", O_RDONLY);
    printf("open success\n");

    while (1) {
        nread = read(fd, buf, 30);

        printf("read %d byte from fifo,context:%s\n", nread, buf);
    }
    close(fd);
    return 0;
}
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
//write
int main() {
    int cnt = 0;
    char *str = "message from fifo";

    int fd = open("./file", O_WRONLY);
    printf("write open success\n");

    while (1) {
        write(fd, str, strlen(str));
        sleep(1);
        cnt++; 
        if (cnt == 5) {
            break;
        }
    }
    close(fd);
    return 0;
}

函数说明
mkfifo()会依参数pathname建立特殊的FIFO文件,该文件必须不存在,而参数mode为该文件的权限(mode%~umask),因此 umask值也会影响到FIFO文件的权限。Mkfifo()建立的FIFO文件其他进程都可以用读写一般文件的方式存取。当使用open()来打开 FIFO文件时,O_NONBLOCK旗标会有影响
1、当使用O_NONBLOCK 旗标时,打开FIFO 文件来读取的操作会立刻返回,但是若还没有其他进程打开FIFO 文件来读取,则写入的操作会返回ENXIO 错误代码。
2、没有使用O_NONBLOCK 旗标时,打开FIFO 来读取的操作会等到其他进程打开FIFO文件来写入才正常返回。同样地,打开FIFO文件来写入的操作会等到其他进程打开FIFO 文件来读取后才正常返回。
返回值
若成功则返回0,否则返回-1,错误原因存于errno中。
错误代码
EACCESS 参数pathname所指定的目录路径无可执行的权限
EEXIST 参数pathname所指定的文件已存在。
ENAMETOOLONG 参数pathname的路径名称太长。
ENOENT 参数pathname包含的目录不存在
ENOSPC 文件系统的剩余空间不足
ENOTDIR 参数pathname路径中的目录存在但却非真正的目录。
EROFS 参数pathname指定的文件存在于只读文件系统内。