问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最古老的形式。
特点:
- 它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。
- 它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。
- 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。
原型:
1 #include <unistd.h>
2 int pipe(int fd[2]); // 返回值:若成功返回0,失败返回-1
当一个管道建立时,它会创建两个文件描述符:fd[0]
为读而打开,fd[1]
为写而打开。如下图:
要关闭管道只需将这两个文件描述符关闭即可。
例子
单个进程中的管道几乎没有任何用处。所以,通常调用 pipe 的进程接着调用 fork,这样就创建了父进程与子进程之间的 IPC 通道。如下图所示:
若要数据流从父进程流向子进程,则关闭父进程的读端(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函数(在下一节说明)取得其终止状态。
等待子进程
为啥要等待子进程退出
父进程等待子进程退出,并收集子进程的退出状态
宏 | 说明 |
---|---|
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进程收留孤儿进程,变成孤儿进程的父进程
相关函数
pid参数的作用解释如下:
pid == -1
等待任 一 子进程。就这一方面而言,waitpid与wait等效。
pid>0
等待其进程ID与pid相等的子进程。
pid==0
等待其组D等于调用进程组D的任一子进程。(9.4节将说明进程组。)
pid <-1
等待其组D等于pid绝对值的任一子进程。
status参数:是一个整型数指针
非空:子进程退出状态放在它所指向的地址中。
空:不关心退出状态
常量 | 说明 |
---|---|
WCONTINUED | 若实现支特作业控制.那么由d指定的任一子进程在暂停后已经继续,但其状态尚未报告, 则返回其状态(POSIX.1的XSI扩展) |
WNOHANG | 若由pid指定的子进程并不是立即可用的,则waitpid不阻塞,此时其返同值为0 |
WUNTRACED | 若某实现支特作业控制,而由d指定的任一子进程已处于暂停状态,并且其状态自暂停以 来还未报告过,则返同其状态。WIFSTOPPED宏确定返回值是否对应干一个暂停子进程 |
- 如果其所有子进程都还在运行,则阻塞。
- 如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。
- 如果它没有任何子进程,则立即出错返回。
区别:
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,也称为命名管道,它是一种文件类型。
- FIFO可以在无关的进程之间交换数据,与无名管道不同。
- 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指定的文件存在于只读文件系统内。