进程
- 进程是一个可并发执行的具有独立功能的程序关于某个数据集合的一次执行过程,也是操作系统执行资源分配和保护的基本单位。
- 程序的一次执行就是一个进程
- 一个程序可以派生多个进程
- 多个不同程序运行的时候,也会有多个相对应的进程与其相互对应
- 进程是动态的,有始有终,有自己的生命周期,有进程状态的变化
程序与进程的区别
- 程序是静止的,无生命的,进程是活动的
- 程序是可以脱离机器长期存在,而进程是执行了的程序
- 程序不具备并发性,不占据系统的资源,进程则相反,具备并发性、会占据内存空间,并且会受到其他进程的制约和影响
- 一个程序对应很多的进程
进程的状态
- 运行状态
- 就绪状态 缺少cpu
- 等待状态 缺少IO资源,等待
相关命令
- 使用命令 ps -ax 查看进程的相关状态;比如进程号 、 进程的状态、
进程树的形成
- 计算机启动之后,BIOS从磁盘引导扇区加载系统引导程序,它将Linux系统装入内存,并且跳到内核处开始执行,Linux内核就执行初始化工作:初始化硬件、初始化内部数据结构、建立进程0
- 进程0创建进程1,进程1是以后所有创建进程的祖先,它负责初始化所有的用户进程。进程1创建shell,shell进程显示提示符,等待命令的输入
- init进程ID为1,通常是init进程,在自检过程结束的时候由内核调用
- init进程不会终止
- init进程是一个普通的用户进程(与交换进程不同,他不是内核中的系统进程,但是它以超级用户特权执行)
进程的创建
- 任何进程的创建,都是基于现有的进程
- 进程的创建可以使用fork 和 exec
- fork 视为新的进程分配响应的数据结构,并且将父进程的相应上下文信息复制过来
- exec 将可执行文件的正文和数据转入内存替代先前的内容(从父进程复制过来的数据),并开始执行正文段,将当前进程替换为一个新的进程
进程的终止
- 使用exit()
- exit释放除了task_struct以外的所有的上下文,父进程收到子进程的终结信号之后,释放子进程的task_struct
- vfork调用之后,将立即调用exec,这样就不需要拷贝父进程的所有的页表,因此比fork快
- pid_t fork(void);
- pid_t vfor(void);
- 头文件 unistd.h
fork系统调用
- 当fork函数调用成功时,对父进程返回子进程的PID,对子进程返回0
- 调用失败的时候,父进程返回-1,并没有子进程的创建
- fork函数返回 两个参数,分别给父进程和子进程
pid_t new_pid;
new_pid = fork();
switch(new_pid){case -1://Error!break;case 0://Child processbreak;default://parent processbreak;
}
exec系统调用
- exec是用来执行一个可执行文件来代替当前进程的执行镜像
- int execl(const char * path,const char * arg, ...)
- int execlp(const char * file,const char * arg, ...)
- int execle(const char *path, const char * arg,...,const char *envp[])
- int execv(const char *path,const char * argv[])
- int execve(const char * filename,char * const argv[],char * const envp[])
- int execvp(const char *file,char * const argv[])
函数的原型
- int execve(const char * filename,char * const argv[],char * const envp[])
- filename 执行的文件
- argv 传递给文件的参数
- envp 传递给文件的环境变量
- 当参数path所指定的文件替换原进程的执行镜像之后,文件的path开始执行,参数argv和envp便传递给进程
- l 表示list
- v表示vector
- e 可以传递新进程环境变量 execle execve
- p 可执行文件查找方式为文件的名字 execlp execvp
char* envp[] = {"PATH = /tmp","User = lei","STATUS = testing", nullptr};char* argv_execv[] = {"echo","executed by execv", nullptr};char* argv_execvp[] = {"echo","executed by execvp", nullptr};char* argv_execve[] = {"env", nullptr};if (execl("/bin/echo","echo","executed by execl", nullptr)<0)perror("Err on execl");if (execlp("echo","echo","executed by execlp", nullptr)<0)perror("Err on execlp");if(execle("/user/bin/env","env", nullptr,envp)<0)perror("Err on execle");if (execv("/bin/echo",argv_execv)<0)perror("Err on execv");if (execvp("echo",argv_execvp))perror("Err on execvp");if(execve("/user/bin/ebv",argv_execve,envp)<0)perror("Err on execve");
函数
- 头文件 <sys/types.h> <unistd.h>
- pid_t getpid(void);返回调用进程的进程ID
- pid_t getppid(void);但会调用进程的父进程ID
- uid_t getuid(void);返回调用进程的实际用户ID
- uid_t geteuid(void);返回进程的有效用户ID
- gid_t getgid(void);返回进程的实际组ID
- gid_t getegit(void);调用进程的有效组ID
#include <iostream>
#include <sys/types.h>
#include <unistd.h>int main(){pid_t cld_pid;cld_pid = fork();if (cld_pid == 0){std::cout << "This is child process.\n" << std::endl;printf("My PID(child) is %d\n",getpid());printf("My child PID is %d\n",cld_pid );}else{std::cout << "This is parent process.\n" << std::endl;printf("My PID(parent) is %d\n",getpid());printf("My child PID is %d\n",cld_pid );}return 0;
}
exit系统调用
- _exit 直接结束进程,清除其内存使用空间,并且清除其在内核中的数据结构
- exit 函数在调用之前会检查文件的打开情况,把文件的缓存区的内容写会文件中,比如调用printf()函数
wait系统调用
- wait函数 用于使父进程阻塞,直到一个进程结束或者该进程收到一个指定信号为止
- 调用wait或者waitpid的进程可能会:
- 阻塞:如果其所有的进程还在运行
- 带子进程的终止状态立即返回(如果一个子进程已经终止,正等待父进程存取其终止状态)
- 出错立即返回(如果没有任何子进程)
- pid_t wait(int *status); status返回子进程退出时的状态信息
- pid_t waitpid(pid_t pid,int *status,int options);//等待指定的进程id
- 头文件 <sys/types.h> 和 <sys/wait.h>
- 两个函数返回数值:如果成功返回子进程的ID号,如果出现问题,返回-1
wait和waitpid函数的区别
- 在一个子进程终止之前,wait使其调用者阻塞,而waitpid有一个选项,可以不阻塞调用者
- waitpid并不等待第一个终止的子进程,他有若干个选择项,可以控制他所等待的特定进程
- 实际上,wait函数是waitpid的一个特例
守护进程
- 创建子进程,然后父进程退出
- 在子进程中创建新的会话 setsid();
- 改变目录为根目录 chdir("/");
- 重设文件的权限掩码umask(0);
- 关闭文件描述符 for(int i = 0;i < MAXFILE; i++){close(i);}
- 守护进程的工作 while(1){}
#include <iostream>
#include <unistd.h>
#include <ftw.h>
#include <fcntl.h>int main(){pid_t pc;char *buf = "This is a Daemon\n";int len = strlen(buf);pc = fork();if (pc < 0){printf("Error fork!\n");exit(-1);} else if (pc > 0){exit(0);}setsid();//在子进程中创建新的会话chdir("/");//改变目录为根目录umask(0);//重设文件的权限掩码for (int i = 0; i < 65535; ++i) {close(i);}int fd;while (1){if (fd = open("/tmp/daemon.log",O_CREAT|O_WRONLY|O_APPEND,0600) < 0){perror("open");exit(1);}write(fd,buf,len+1);close(fd);sleep(10);}return 0;
}
sleep函数
- 函数使用sleep用来指定进程挂起来的指定的秒数。该函数的调用格式如下
- unsigned int sleep(unsigned int seconds);
- 头文件 unistd.h
进程间通信
- 数据传输:一个进程将 数据 发送给另外一个进程,数据大小一般是一个字节到几兆字节之间
- 共享数据:多个进程之间操作共享数据,一个进程对共享数据的修改,另外的进程可以看到
- 通知事件:一个进程向另外一个进程发送消息,通知它发生了某件事情。(如:子进程终止,通知父进程)
- 资源共享:多个进程之间共享相同的资源,需要内核提供 锁 和 同步机制
- 进程控制:有些进程完全控制另一个进程的执行(如Debug),这个时候,进程会拦截另外一个进程所有的陷入(陷入内核)和异常,并且及时知道他的状态改变
- 进程间通信 (IPC)
- Linux进程间通信 包括Unix进程间通信、Socket进程间通信、POSIX进程间通信、System V进程间通信
- Unix进程间通信 -> 管道 FIFO 信号
- System V进程间通信 -> System V消息队列 System V信号灯 System V共享内存
- POSIX进程间通信 -> posix消息队列 posix信号灯 posix共享内存
现在的linux使用的进程间通信方式
- 管道(pipe) 和 有名管道(FIFO)
- 信号(signal):比较复杂的通信方式,用于通知其余进程,信号可以用于进程之间通信外,信号还可以发送信号给进程本身。
- 消息队列:消息的链接表,包括posix消息队列和system V消息队列;有权限的进程可以读写消息队列;克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺陷
- 共享内存:多个进程访问同一块内存空间,是最快的IPC方式,往往结合信号量来实现进程间同步以及互斥
- 信号量 (使用不多)
- 套接字(socket) (网络)
管道和有名管道的区别
- 管道:具有亲缘关系的进程间通信,需要通信双方的进程有共同的祖先进程
- 有名管道:允许无亲缘关系的管道之间通信,在文件系统中有对应的文件名字;有名管道通过命令 mkfifo或系统调用mkfifo来创建
具体使用
管道
- linux命令允许重定向,重定向就是管道 例如 ls > 1.txt
- 管道是单向的、先进先出的,无结构的、固定大小的字节流,他把一个进程的标准输出和另外一个进程的标准输入连接在一起
- 写进程在管道的末尾输入数据,读进程在管道的首端读取数据。数据被读出后会从管道中移除,其他进程对此不可见了
- 管道提供了简单的流控制机制。进程试图读取空的管道时,在有数据填充管道之前会处于阻塞状态;同理,管道数据已满时,未有读进程读取数据,写进程往管道里面写入数据会处于阻塞状态
- 管道用于进程之间通信
- 使用系统调用pipe() 函数,创建一个简单的管道,只需接受一个参数,也就是一个包括两个整数的数组。
- 如果系统调用成功,此数组将包括管道使用的两个文件描述符
- 创建一个管道之后,一般情况下进程将会产生一个新的进程
- 系统调用pip() 原型:int pipe(int fd[2])
- 返回值:成功返回0 ;失败返回 -1
- errno = EMFILE 没有空闲的文件描述符
- errno = ENFILE 系统文件表已满
- errno = EFAULT fd数组无效
代码
#include <stdio.h>
#include <unistd.h>int main(void)
{int pipe_fd[2];if(pipe(pipe_fd) < 0){printf("pipe create error!\n");return -1;} else{printf("pipe create success!\n");}close(pipe_fd[0]);close(pipe_fd[1]);return 0;
}
- 先创建一个管道 再创建一个子进程
父子进程之间通信 父进程写入;子进程读入数据
- 当读到10的时候,结束进程
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>int main(void)
{int pipe_fd[2];pid_t pid;char buf[2]={0};//在创建子进程之前创建管道if(pipe(pipe_fd) < 0){printf("pipe create error!\n");_exit(-1);} else{printf("pipe create success!\n");printf("pipe_fd[0] = %d,pipe_fd[1] = %d",pipe_fd[0],pipe_fd[1]);}pid = fork();//创建子进程//子进程执行的代码if (pid == 0){close(pipe_fd[1]);while (1){if (read(pipe_fd[0],buf,1) > 0){
// if (buf[0] % 3 == 0 && buf[0] != 0)printf("%d\n",buf[0]);if (buf[0] == 10)_exit(0);}}close(pipe_fd[0]);} else{//父进程执行的代码close(pipe_fd[0]);for (buf[0]=0;buf[0]<=10;buf[0]++) {write(pipe_fd[1],buf,1);}close(pipe_fd[1]);wait(nullptr);}return 0;
}
上面这个例子 感觉有问题
#include <stdio.h>
#include <unistd.h>
#include <wait.h>
#include <cstring>int main(){int pipe_fd[2];if (pipe(pipe_fd) < 0){printf("pipe create error!\n");_exit(-1);return -1;} else{printf("pipe create success!\n");printf("pipe_fd[0] = %d,pipe_fd[1] = %d",pipe_fd[0],pipe_fd[1]);}pid_t pid = fork();
// char buf[2] = {0};char buf[100] ={"Hello world"};int len = strlen(buf);if (pid == 0){close(pipe_fd[1]);while(1){if (read(pipe_fd[0],buf,len) > 0){printf("%s\n",buf);}}close(pipe_fd[0]);} else{//父进程执行此段代码close(pipe_fd[0]);for (int i = 0;i<10;i++){write(pipe_fd[1], buf, len);}close(pipe_fd[1]);wait(nullptr);}return 0;
}
简单修改如下
- 通过打开两根管道,实现一个双向的管道,但是需要在子进程中正确的设置文件的描述符号
- 必须在fork之前使用pipe(),否则子进程将不会继承文件的描述符
- 使用半双工管道的时候,任何关联的进程都需要共享一个相关的祖先进程。因为管道存储在系统内核中,不继承祖先将无法寻址,有名管道就不会出现这个问题
代码
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>int main(void)
{int pipe_fd[2];int pipe_fd1[2];pid_t pid;char buf_r[100]={0};char * p_wbuf = nullptr;int r_num;//在创建子进程之前创建管道if(pipe(pipe_fd) < 0){printf("pipe create error!\n");_exit(-1);}if(pipe(pipe_fd1) < 0){printf("pipe create error!\n");_exit(-1);}printf("pipe create success!\n");
// printf("pipe_fd[0] = %d,pipe_fd[1] = %d",pipe_fd[0],pipe_fd[1]);
// printf("pipe_fd1[0] = %d,pipe_fd1[1] = %d",pipe_fd1[0],pipe_fd1[1]);pid = fork();//创建子进程//子进程执行的代码if (pid == 0){printf("******子进程******\n");//read pipeclose(pipe_fd[1]);r_num = read(pipe_fd[0],buf_r,100);if (r_num > 0){printf("children r_num = %d, pipe is: %s\n",r_num,buf_r);}close(pipe_fd[0]);//write pipeclose(pipe_fd1[0]);if (write(pipe_fd1[1],"ByeBye!",7)!=-1){printf("children process write ByeBye! success!\n");}close(pipe_fd1[1]);printf("children process exit!\n");_exit(0);} else if (pid > 0){printf("******父进程******\n");//write pipeclose(pipe_fd[0]);if (write(pipe_fd[1],"Hello!",6) !=-1){printf("father process write Hello! success!\n");}close(pipe_fd[1]);//read processclose(pipe_fd1[1]);r_num = read(pipe_fd1[0],buf_r,100);if (r_num > 0){printf("father r_num = %d, pipe is: %s\n",r_num,buf_r);}close(pipe_fd1[0]);wait(nullptr);printf("father process exit!\n");_exit(0);}return 0;
}
- 父进程 给 子进程 传输 Hello!;接收输出子进程的 ByeBye!
- 子进程 给 父进程 传输 ByeBye!;接收输出父进程的 Hello!
- 子进程退出,然后父进程退出
- r_num 为读取的数据长度
命名管道
- 作为一个特殊的设备文件而存在
- 适用于无血缘关系的进程之间通信
- 即使进程之间不再需要管道,但是命名管道仍然继续保存在文件系统中,便于以后使用
- 使用函数 int mkfifo(const char * pathname,mode_t mode)
- 函数说明:mkfifo()会依参数pathname 建立特殊的FIFO 文件, 该文件必须不存在, 而参数mode 为该文件的权限 (mode%~umask), 因此umask 值也会影响到FIFO 文件的权限. Mkfifo()建立的FIFO 文件其他进程都可以用读写一般文件的方式存取. 当使用open()来打开FIFO 文件时, O_NONBLOCK 旗标会有影响:
- 头文件:#include <sys/types.h> #include <sys/stat.h>
- 1. 当使用O_NONBLOCK 旗标时, 打开FIFO 文件来读取的操作会立刻返回, 但是若还没有其他进程打开FIFO 文件来读取, 则写入的操作会返回ENXIO 错误代码.
- 2. 没有使用O_NONBLOCK 旗标时, 打开FIFO 来读取的操作会等到其他进程打开FIFO 文件来写入才正常返回. 同样地, 打开FIFO 文件来写入的操作会等到其他进程打开FIFO 文件来读取后才正常返回.
- 返回值:若成功则返回0, 否则返回-1, 错误原因存于errno 中
- 使用ls -l查看管道文件 显示p开头
- 普通文件 是一个 -
错误代码:
- 1、EACCESS 参数pathname 所指定的目录路径无可执行的权限
- 2、EEXIST 参数pathname 所指定的文件已存在.
- 3、ENAMETOOLONG 参数pathname 的路径名称太长.
- 4、ENOENT 参数pathname 包含的目录不存在
- 5、ENOSPC 文件系统的剩余空间不足
- 6、ENOTDIR 参数pathname 路径中的目录存在但却非真正的目录.
- 7、EROFS 参数pathname 指定的文件存在于只读文件系统内.
注意事项
- 打开FIFO,使用非阻塞标志(O_NONBLOCK)产生以下的影响
- 如果未说明O_NONBLOCK,只读打开会要阻塞某个写进程打开此FIFO;类似,只写打开要阻塞某个读进程打开此FIFO;读写互斥
- 如果指定了O_NONBLOCK,则只读打开立即返回,但是如果没有进程为了读而打开一个FIFO,那么只写打开将出错返回,其errno是ENXIO
- 类似管道,若写一个尚无进程为读而打开的FIFO,则产生信号为SIGPIPE;若某个FIFO最后一个写进程关闭了该FIFO,则将为该FIFO的读进程产生一个文件结束标志
参考链接
- Linux进程描述符task_struct结构体详解--Linux进程的管理与调度(一)
- Linux 系统启动过程
- Linux系统启动流程详解
- fork和exec的区别
- Linux中fork,vfork和clone详解(区别与联系)
- C语言vfork()函数:建立新的进程
- linux系统编程之进程(五):exec系列函数(execl,execlp,execle,execv,execvp)使用
- C语言waitpid()函数:中断(结束)进程函数(等待子进程中断或
- linux守护进程详解及创建,daemon()使用
- linux系统的7种运行级别
- SGX的内部组件概述(一)找一个好的工作,为自己活
- C语言mkfifo()函数:建立具名管道
- linux驱动重要头文件!!!
- errno头文件