Linux进程和系统调用
一、系统调用
1.系统调用是什么
- 系统调用跟用户自定义函数一样也是一个函数,不同的是系统调用运行在内核态,而用户自定义函数运行在用户态。由于某些指令(如设置时钟、关闭/打开中断和I/O操作等)只能运行在内核态,所以操作系统必须提供一种能够进入内核态的方式,系统调用就是这样的一种机制
- 系统调用是Linux 内核提供的一段代码(函数),其实现了一些特定的功能,用户可以通过 int 0x80 中断(x86 CPU)或者 syscall指令(x64 CPU)来调用系统调用。
- 内核提供用户空间程序与内核空间进行交互的一套标准接口,这些接口让用户态程序能受限访问硬件设备,比如申请系统资源,操作设备读写,创建新进程等。用户空间发生请求,内核空间负责执行,这些接口便是用户空间和内核空间共同识别的桥梁,这里提到两个字“受限”,是由于为了保证内核稳定性,而不能让用户空间程序随意更改系统,必须是内核对外开放的且满足权限的程序才能调用相应接口
- 在用户空间和内核空间之间,有一个叫做syscall(系统调用, system call)的中间层,是连接用户态和内核态的桥梁。这样即提高了内核的安全型,也便于移植,只需实现同一套接口即可。Linux系统,用户空间通过向内核空间发出syscall,产生软中断,从而让程序陷入内核态,执行相应的操作。对于每个系统调用都会有一个对应的系统调用号,比很多操作系统要少很多
- 系统调用是用户态进入内核态的唯一入口:一夫当关,万夫莫开。常用系统调用:
- 控制硬件:如write/read调用。
- 设置系统状态或读取内核数据——getpid()、getpriority()、setpriority()、sethostname()
- 进程管理:如 fork()、clone()、execve()、exit()等
2.为什么需要系统调用
-
人跟人之间是没有信任的,你怎么确定进程能够按照你的想法使用操作系统,所以
-
系统调用提供了一种硬件的抽象接口,使得进程不需要关系磁盘类型和介质等底层的信息。
-
作为操作系统和进程的中间层,操作系统可以基于权限、用户类型和其他的一些规则对访问进行限制
-
二、进程
1.进程是什么
- 进程是操作系统分配资源的基本单位,要产生一个进程,有多种产生方式,例如使用fork()函数、system()函数、exec()函数等,这些函数的不同在于其运行环境的构造之间存在差别,其本质都是对程序运行的各种条件进行设置,在系统之间建立一个可以运行的程序
- 进程是指计算机中已运行的程序。进程本身不是基本的运行单位,而是线程的容器。程序本身只是指令、数据及其组织形式的描述,进程才是程序(那些指令和数据)的真正运行实例
2.为什么需要进程
- 不是进程需要我们,而是我们需要进程。想象一下,现在提供一个操作系统,而你想要通过操作系统控制各种资源,包括处理器、内存、磁盘等,你会怎么做?
- 将各个资源排队,每次都取一类资源处理,最后整和到一起使用,这得是一种多么低效的手段
- 提前打包好各种可能使用的资源,每次使用申请一个已经包装好各个资源的壳子出来,这就聪明很多了嘛
3.进程的重点
-
概念性的东西,笔者一向会有点抓不住重点,所以画个重点:
-
进程是处于执行时期的程序。
-
内核调度的对象是线程,不是进程。
-
-
结论:操作系统作为硬件的使用层,提供使用硬件资源的能力,进程作为操作系统使用层,提供使用操作系统抽象出的资源层的能力。
4.进程号
-
每个进程在初始化的时候,系统都分配了一个ID号,用于标识此进程。在Linux中进程号是唯一的,系统可以用这个值来表示一个进程,描述进程的ID号通常叫做PID,即进程ID(process id)。PID的变量类型为pid_t,类型pid_t其实是一个typedef类型,定义为unsigned int。
-
getpid()、getppid()函数介绍
-
getpid()函数返回当前进程的ID号;getppid()返回当前进程的父进程的ID号
-
getpid()函数和getppid()函数的原型如下
#include <sys/types.h> #include <unistd.h> pid_t getpid(void); pid_t getppid(void);
-
下面是一个使用getpid()函数和getppid()函数的例子。程序获取当前程序的PID和父程序的PID
#include<stdio.h> #include<unistd.h> #include<sys/types.h> int main() { pid_t pid,ppid; //获得当前进程和其父进程的ID号 pid = getpid(); ppid = getppid(); printf ("当前进程的ID号为:%d\n",pid); printf ("当前进程的的父进程号ID号为:%d\n",ppid); return 0; }
-
对上述程序进行编译,在系统上进行运行,其结果为
-
可以知道,进程的ID号为13345,其父进程的ID号为12378。在当前系统上使用ps和 grep进行进程12378的查找,可以知道,ID号为12378的进程为bash,即当前环境中的脚本程序。查找其父进程的命令
因为是在当前bash中运行的此程序,所以ID 13345其父进程为bash
-
5.进程复制fork()
-
产生进程的方式比较多,fork()是其中的一种方式。fork()函数以父进程为蓝本复制一个进程,其ID号和父进程ID号不同。在Linux环境下,fork()是以写复制实现的,只有内存等与父进程不同,其他与父进程共享,只有在父进程或者子进程进行了修改后,才重新生成一份
-
fork()函数介绍
-
fork()函数的原型如下,当成功时,fork()函数的返回值是进程的ID;失败则返回-1
#linclude <sys/types.h> #include <unistd.h> pid_t fork(void);
-
fork()的特点是执行一次,返回两次。在父进程和子进程中返回的是不同的值,父进程返回的是子进程的ID号,而子进程中则返回0。
-
-
fork()函数的例子
-
在调用fork()函数之后,判断fork()函数的返回值: 如果为-1,打印失败信息;如果为0,打印子进程信息;如果大于0,打印父进程信息。
#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<sys/types.h> int main() { pid_t pid; //分叉进程 pid = fork(); //判断是否执行成功 if(pid == -1) { printf("进程创建失败!\n"); } else if(pid == 0) { //子进程中执行此段代码 printf("子进程,fork返回:%d,ID:%d,父进程ID:%d\n",pid,getpid(),getppid()); exit(0); } else { wait(NULL); //父进程中执行此段代码 printf("父进程,fork返回:%d,ID:%d,父进程ID:%d\n",pid,getpid(),getppid()); } return 0; }
-
执行此段程序的结果为
fork出来的子进程的父进程ID号是执行fork()函数的进程的ID号
-
6.开启另一个进程system()
-
system()函数调用shell的外部命令在当前进程中开始另一个进程
-
system()函数介绍
-
system()函数调用/bin/sh-c command执行特定的命令,阻塞当前进程直到command命令执行完毕
-
system()函数的原型
#linclude <stdlib.h> int system (const char *command);
-
-
执行system()函数时,会调用fork()、execve()、waitpid()等函数,其中任意一个调用失败,将导致system()函数调用失败
-
system()函数的返回值如下:
- 失败,返回-1;
- 当sh不能执行时,返回127;
- 成功,返回进程状态值
-
当system接受的命令为NULL时直接返回,否则fork出一个子进程,因为fork在两个进程(父进程和子进程)中都返回,这里要检查返回的 pid,fork在子进程中返回0,在父进程中返回子进程的pid,父进程使用waitpid等待子进程结束,子进程则是调用execl来启动一个程序代替自己,execl(“/bin/sh”, “sh”, “-c”, cmdstring,(char*)0)是调用shell,这个shell的路径是/bin/sh,后面的字符串都是参数,然后子进程就变成了一个 shell进程,这个shell的参数是cmdstring,就是system接受的参数。
-
system(执行shell 命令)
-
相关函数
fork,execve,waitpid,popen
-
表头文件
#include<stdlib.h>
-
定义函数
int system(const char * string);
-
函数说明
system()会调用fork()产生子进程,由子进程来调用/bin/sh-c string来执行参数string字符串所代表的命令,此命令执行完后随即返回原调用的进程。在调用system()期间SIGCHLD 信号会被暂时搁置,SIGINT和SIGQUIT 信号则会被忽略。
-
返回值
如果system()在调用/bin/sh时失败则返回127,其他失败原因返回-1。若参数string为空指针(NULL),则返回非零值。如果 system()调用成功则最后会返回执行shell命令后的返回值,但是此返回值也有可能为system()调用/bin/sh失败所返回的127,因此最好能再检查errno 来确认执行成功。
-
附加说明
在编写具有SUID/SGID权限的程序时请勿使用system(),system()会继承环境变量,通过环境变量可能会造成系统安全的问题
-
-
system()函数的例子
-
例如下面的代码获得当前进程的ID,并使用system()函数进行系统调用ping网络上的某个主机,程序中将当前系统分配的PID值和进行system()函数调用的返回值都进行了打印
#include<stdio.h> #include<unistd.h> #include<stdlib.h> int main() { int ret; printf("系统分配的进程号是:%d\n",getpid()); ret = system("ping www.baidu.com -c 4"); printf("返回值为:%d\n",ret); return 0; }
对上述代码进行编译,执行编译后的结果,其执行结果为
-
系统分配给当前进程的ID号为5055,然后系统ping 了百度,发送和接收四个ping的请求包,再退出ping程序;此时系统的返回值在原来的程序中才返回,在测试的时候返回的是0
-
7.进程执行exec()
-
在使用fork()函数和system()函数的时候,系统中都会建立一个新的进程,执行调用者的操作,而原来的进程还会存在,直到用户显式地退出;而exec()族的函数与之前的fork()和system()函数不同,exec()族函数会用新进程代替原有的进程,系统会从新的进程运行,新进程的PID值会与原来进程的PID值相同
-
exec()函数介绍
exec()族函数共有7个,具体如下
int execl(); int execlp(); int execle(); int execv(); int execvp(); int execvpe(); int execve();
-
上述7个函数中,只有execve()函数是真正意义上的系统调用,其他6个函数都是在此基础上经过包装的库函数。上述的exec()函数族的作用是,在当前系统的可执行路径中根据指定的文件名来找到合适的可执行文件名,并用它来取代调用进程的内容,即在原来的进程内部运行一个可执行文件。上述的可执行文件既可以是二进制的文件,也可以是可执行的脚本文件。
-
与fork()函数不同,exec()函数族的函数执行成功后不会返回,这是因为执行的新程序已经占用了当前进程的空间和资源,这些资源包括代码段、数据段和堆栈等,它们都己经被新的内容取代,而进程的ID等标识性的信息仍然是原来的东西,即exec()函数族在原来进程的壳上运行了自己的程序,只有程序调用失败了,系统才会返回-1。
-
使用exec()函数比较普遍的一种方法是先使用fork()函数分叉进程,然后在新的进程中调用exec()函数,这样exec()函数会占用与原来一样的系统资源来运行。
-
Linux系统针对上述过程专门进行了优化。由于fork()的过程是对原有系统进行复制,然后建立子进程,这些过程都比较耗费时间。如果在fork()系统调用之后进行exec()系统调用,系统就不会进行系统复制,而是直接使用exec()指定的参数来覆盖原有的进程。上述的方法在Linux系统上叫做“写时复制”,即只有在造成系统的内容发生更改的时候才进行进程的真正更新
-
execve()函数的例子
-
execve()函数的例子如下。示例程序中先打印调用进程的进程号,然后调用execve()函数,这个函数调用可执行文件”/bin/ls”列出当前目录下的文件
#include<stdio.h> #include<unistd.h> int main() { char *args[] = {"/bin/ls",NULL}; printf("系统分配的进程号是:%d\n",getpid()); if(execve("/bin/ls",args,NULL)<0) { printf("创建进程出错!\n"); } return 0; }
执行结果如下所示
-
8.所有用户进程的产生进程init/systemd
-
init
-
以前的Linux启动都是用init进程。启动服务:
$ sudo /etc/init.d/apache2 start # 或者 $ service apache2 start
-
缺点:
-
启动时间长。init进程是串行启动,只有前一个进程启动完,才会启动下一个进程。
-
启动脚本复杂。init进程只是执行启动脚本,不管其他事情。脚本需要自己处理各种情况,这往往使得脚本变得很长。
-
-
-
systemd
-
在较新的linux系统上,都使用systemd 取代了init,成为系统的第一个进程(PID 等于 1),其他进程都是它的子进程
-
systemd为系统启动和管理提供了完整的解决方案。它提供了一组命令。字母d是守护进程(daemon)的缩写。查看systemd 的版本
systemctl --version systemd 219 +PAM +AUDIT +SELINUX +IMA -APPARMOR +SMACK +SYSVINIT +UTMP +LIBCRYPTSETUP +GCRYPT +GNUTLS +ACL +XZ +LZ4 -SECCOMP +BLKID +ELFUTILS +KMOD +IDN
-
systemd作用:systemd提供一组命令,这些涉及到linux系统的各方各面
-
-
在Linux系统中,所有的进程都是有父子或者堂兄关系的,除了初始进程init/systemd,没有哪个进程与其他进程完全独立。系统中每个进程都有个父进程,新的进程不是被全新地创建,通常是从一个原有的进程进行复制或者克隆的。
-
Linux操作系统下的每一个进程都有一个父进程或者兄弟进程,并且有自己的子进程。可以在Linux下使用命令pstree来查看系统中运行的进程之间的关系,如下所示。可以看出,systemd进程是所有进程的祖先,其他的进程都是由systemd进程直接或者间接fork()出来的。