系统调用与进程
系统调用与进程
1. 系统调用
1.1 系统调用概述
1.1.1 什么是系统编程
操作系统的职责:
操作系统用来管理所有的资源,并将不同的设备和不同的程序关联起来
什么是Linux系统编程:
在有操作系统的环境下编程,并使用操作系统提供的系统调用及各种库(c开发),对系统资源进行访问
c语言+系统调用的函数,就可以进行Linux系统编程
1.1.2 什么是系统调用
系统调用是操作系统提供给用户程序的一组"特殊"函数接口
Linux的不同版本提供了丰富(200+)的系统调用,用户程序可以通过这些接口获得操作系统(内核)提供的服务。例如,用户可以通过文件系统相关的系统调用,请求系统打开文件、关闭文件或读写文件。
系统调用按功能逻辑大致可分为:
进程控制、进程间通信、文件系统控制、系统控制、内存管理、网络管理、socket控制、用户管理
系统调用的返回值:
通常,用负的返回值来表明错误,返回0值表明成功。
错误信息存放在全局变量errno中,用户可用perror函数打印出错信息。
系统调用遵循的规范:
在Linux中,应用程序编程接口(API)遵循POSIX标准
POSIX标准基于当时现有的UNIX实践和经验,描述了操作系统的系统调用编程接口(实际上就是API),用于保证应用程序可以在源代码一级上在多种操作系统上移植运行。
1.2 系统调用I/O函数
系统调用中操作I/O的函数,都是针对文件描述符的
通过文件描述符可以直接对相应的文件进行操作(open,close,write,read,ioctl)
1.2.1 文件描述符
文件描述符是非负整数。
打开现存文件或新建文件时,系统(内核会返回一个文件描述符)
文件描述符用来指定已打开的文件
三个标准描述符:默认打开的
#define STDIN_FILENO 0 //标准输入的文件描述符
#define STDIN_FILENO 1 //标准输出的文件描述符
#define STDIN_FILENO 0 //标准错误的文件描述符
【扩展】
ulimit -a
查看open files打开的文件最大数
ulimit -n 最大数
设置open files打开的文件最大数
1.2.2 open函数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname,int flags);文件存在时
int open(const char *pathname,int flags,mode_t mdoe);文件不存在时
参数说明
pathname:文件的路径及文件名
flags:open函数的行为标志
mode:文件权限(可读、可写、可执行)的设置
返回值
成功返回打开的文件描述符
失败返回-1,可以利用perror去查看原因
flags的取值及其含义
O_RDONLY 以只读的方式打开
O_WRONLY 以只写的方式打开
O_RDWR 以可读、可写的方式打开
flags除了取上述值外,还可与下列值位或
O_CREAT 文件不存在则创建文件,使用此选项时需使用mode说明文件的权限
O_EXCL 确保创建操作是原子的:如果同时指定O_CREAT,且文件已经存在,则出错(执行了create创建时,因为文件已存在则报错)
O_TRUNC 如果文件存在,则清空文件内容
O_APPEND 写文件时,数据添加到文件末尾
O_NONBLOCK 当打开的文件是FIFO、字符文件、块文件时,此选项为非阻塞标志位
mode的取值及其含义
【查看权限掩码】umask
umask:查看当前的权限掩码
umsak mode:设置掩码,mode为八进制 0ddd
umask -S:查看各组用户的默认操作权限
1.2.3 close函数
关闭一个文件
#include <unistd.h>
int close(int fd);
fd是调用open打开文件返回的文件描述符。成功返回0,失败返回-1。
1.2.4 write函数
把指定数目的数据写到文件
#include <unistd.h>
ssize_t write(int fd,const void *addr,size_t count);
参数说明:
fd:文件描述符。
addr:数据首地址
count:写入数据的字节个数
成功返回实际写入数据的字节个数,失败返回-1。
1.2.5 read函数
把指定数目的数据读到内存,默认是阻塞的(如果读不到数据,将阻塞不继续执行,直到有数据可读,才继续往下执行)。
非阻塞特性:如果没数据,立即返回,继续执行
#include <unistd.h>
ssize_t read(int fd, void *addr,size_t count);
成功返回实际读取到的字节个数。失败返回-1,可以利用perror去查看原因
【扩展】文件阻塞特性
1)尝试从/dev/tty
终端读取数据(非阻塞) (先printf()打印数据,再读)
2)int fcntl(int fd,int针对文件描述符进行控制
1.2.6 remove库函数
删除文件或空目录
#include <stdio.h>
int remove(const char *pathname);
成功返回0.失败返回-1,可以利用perror去查看原因
1.3 系统调用与库函数
库函数由两类函数组成:不需要调用 系统调用和需要调用 系统调用
不需要调用系统调用:
不需要切换到内核空间即可完成函数全部功能,并且将结果反馈给应用程序,如strcpy、bzero等字符串操作函数
需要调用系统调用:
需要切换到内核空间,这类函数通过封装系统调用去实现相应功能,如printf、fread等
库函数与系统调用的关系:
并不是所有的系统调用都被封装成了库函数,系统提供的很多功能都必须通过系统调用才能实现
系统调用是需要时间的,程序中频繁的使用系统调用会降低程序的运行效率
当运行内核代码时,CPU工作在内核态,在系统调用发生前需要保存用户态的栈和内存环境,然后转入内核态工作。系统调用结束后,又要切换回用户态。这种环境的切换会消耗掉许多时间。
库函数访问文件的时候根据需要,设置不同类型的缓冲区,从而减少了直接调用IO系统调用的次数,提高了访问效率。
2. 进程
2.1 进程概述
2.1.1 进程的定义
程序:是存放在存储介质上的一个可执行文件。【静态的】
进程:是程序的执行实例,包括程序计数器、寄存器和变量的当前值。【动态的】
程序是一些指令的有序集合,而进程是程序执行的过程。进程的状态是变化的,其包括进程的创建、调度和消亡。
在Linux系统中,进程是管理事务的基本单元。进程拥有自己独立的处理环境和系统资源(处理器、存储器、I/O设备、数据、程序)。
可使用exec函数由内核将程序读入内存,使其执行起来成为一个进程
2.1.2 进程的状态及转换
进程整个生命周期可以简单划分为三种状态
就绪态:进程已经具备执行的一切条件,正在等待分配CPU的处理时间。
执行态:该进程正在占用CPU运行
等待态:进程因不具备某些执行条件而暂时无法继续执行的状态。
2.1.3进程控制块
进程控制块(PCB,Process Control Block)
OS是根据PCB来对并发执行的进程进行控制和管理的。系统在创建一个进程的时候会开辟一段内存空间存放与此进程相关的PCB数据结构。
PCB是操作系统中最重要的记录型数据结构。PCB中记录了用于描述进程进展情况及控制进程运行所需的全部信息
PCB是进程存在的唯一标志,在Linux中PCB存放在task_struct
结构体中。
2.2 进程控制
2.2.1 进程号
每个进程都由一个进程号来标识,其类型为pid_t
,进程号范围:0~32767。
进程号总是唯一的,但进程号可以重用。当一个进程终止后,其进程号就可以再次使用
在Linux系统中进程号由0开始:
进程号为0及1的进程由内核创建
进程号为0的进程通常是调度进程,常被称为交换进程(swapper)。
进程号为1的进程通常是init进程
除调度进程外,在linux下面所有的进程都由进程init进程直接或者间接创建
进程号(PID):标识进程的一个非负整数
父进程号(PPID):任何进程(除init进程)都是由另一个进程创建,该进程称为被创建进程的父进程,对应的进程号称为父进程(PPID)
进程组号(PGID):进程组是一个或多个进程的集合。他们之间相互关联,进程组可以接收同一终端的各种信号,关联的进程有一个进程组号(PGID)
Linux操作系统提供了三个获得进程号的函数getpid()、getppid()、getpgid()
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void) 获取本进程号(PID)
pid_t getppid(void) 获取父进程号(PPID)
pid_t getpgid(pid_t pid) 获取进程组号(PGID),参数为0时返回当前PGID,否则返回参数指定的进程的PGID
【扩展】无法停止程序的运行,按ctrl+z或ctrl+c停止
或者通过kill -9 进程号
杀死进程
2.2.2 fork函数
fork函数是创建子进程的函数
在Linux环境下,创建进程的主要方法是调用以下两个函数
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);独立空间
pid_t vfork(void);共享父进程空间
成功:子进程中返回0,父进程中返回子进程ID;失败:返回-1
使用fork函数得到的子进程是父进程的一个复制品,他从父进程处继承了整个进程的地址空间。地址空间:包括进程上下文、进程堆栈、打开的文件描述符、信号控制设定、进程优先级、进程组号等。子进程所独有的只有它的进程号、计时器等。因此,使用fork函数的代价是很大的。
【特别说明】BSS(Block Started by Symbol)区段是一种用于存储未初始化全局变量和静态变量的内存区域。在程序加载时,BSS区的内存会被系统初始化为零或空值。
调用fork函数后,父进程打开的文件描述符都被复制到子进程中。在重定向父进程的标准输出时。子进程的标准输出也被重定向。write函数是系统调用,不带缓冲
2.2.3 进程的挂起
进程在一定的时间内没有任何动作,称为进程的挂起
#include <unistd.h>
unsigned int sleep(unsigned int sec);
进程挂起指定的描述,直到指定的时间用完或收到信号才解除挂起
若进程挂起到sec指定的时间则返回0,若有信号中断则返回剩余秒数
【注意】进程挂起指定的秒数后程序并不会立即执行,系统只是将此进程切换到就绪态。
2.2.4 进程的等待
父子进程有时需要简单的进程间同步,如父进程等待子进程的结束。
Linux下提供了以下两个等待函数wait()、waitpid()
#include <sys/types.h>
#include <sys/wait.h>
1. wait函数
pid_t wait(int *status);
功能说明:
等待子进程终止,如果子进程终止了,此函数会回收子进程的资源
调用wait函数的进程会挂起,直到它的一个子进程退出或收到一个不能忽视的信号时才被唤醒。若调用进程没有子进程或它的子进程已经结束,该函数立即返回。
参数:
函数返回时,参数status中包含子进程退出时的状态信息。
子进程的退出信息在一个int中包含了多个字段,用宏定义可以取出其中的每个字段
返回值:
如果执行成功则返回子进程的进程号。
出错返回-1,失败原因存于errno中。
取出子进程的退出信息:
WIFEXITED(status) 如果子进程是正常终止的,取出的字段值非零
WEXITSTATUS(status) 返回子进程的退出状态,退出状态保存在status变量的8~16位。在用此宏前应先用宏WIFEXITED判断子进程是否正常退出,正常退出才可以使用此宏。
【注】此status是个wait的参数指向的整型变量
2. waitpid函数
pid_t waitpid(pid_t pid,int *status,int options)
功能:等待子进程终止,如果子进程终止了,此函数会回收子进程的资源。
返回值:如果执行成功则返回子进程ID,出错返回-1,失败原因存于errno中
参数pid的值有以下几种类型:
1)pid>0:等待进程ID等于pid的子进程
2)pid=0:等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会等待它。
3)pid=-1:等待任一子进程,此时waitpid和wait作用一样。
4)pid<-1:等待指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值
status参数中包含子进程退出时的状态信息
options参数能进一步控制waitpid的操作:
0:同wait,阻塞父进程,等待子进程退出
WNOHANG:没有任何已经结束的子进程,则立即返回
WUNTRACED:如果子进程暂停了则此函数马上返回,并且不予以理会子进程的结束状态。【跟踪调试,很少用到】
返回值:
1)成功:返回状态改变了的子进程的进程号:如果设置了选项WNOHANG并且pid指定的进程存在则返回0。
2)出错:返回-1。当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD。
2.2.5 进程的终止
#include <stdlib.h>
void exit(int status); status:返回给父进程的参数(低8位有效)【库函数】
#include <unistd.h>
void _exit(int status); 同exit功能,【系统调用】
2.2.6 进程退出清理
进程在退出前可以用atexit()函数注册退出处理函数
#include <stdlib.h>
int atexit(void (*function)(void));
注册进程正常结束前调用的函数,进程退出执行注册函数。
function:进程结束前,调用函数的入口地址
一个进程中可以多次调用atexit函数注册清理函数,正常结束前调用函数的顺序和注册时的顺序相反。
ps -aux查看进程状态
-a 显示终端上的所有进程,包含其他用户的进程
-u 显示进程的详细状态
-x 显示没有控制终端的进程
-w 显示加宽,以显示更多的信息
-j 显示父进程号、进程组号等信息
stat的含义:
D 不可中断(Uninter ruptible)
R 正在运行,或在队列中的进程
S 处于休眠状态
T 停止或被追踪
Z 僵尸进程
W 进入内存交换(内核2.6+ 无效)
X 死掉的进程
< 高优先级
N 低优先级
s 包含子进程
+ 位于前台的进程组
特殊的几个进程
僵尸进程:
子进程退出,父进程米有回收子进程资源,子进程为僵尸进程。(有危害)
子进程的PID被占用,系统的PID是有数量限制
孤儿进程:
父进程先结束,子进程为孤儿进程(无害的)
被1号进程接管(当孤儿进程结束时,1号进程负责回收其资源)
守护进程(后台进程):是脱离终端的孤儿进程。(关闭0/1/2标准的文件描述,设置当前的进程新会话setsid()、改变当前终端工作目录chdir())
2.2.7 vfork函数
pid_t vfork(void)
vfork函数和fork函数一样都是在已有的进程中创建一个新的进程,但它们创建的子进程是有区别的。创建子进程成功,则在子进程中返回0,父进程中返回子进程ID。出错则返回-1.
fork和vfork函数的区别
1)vfork保证子进程先运行,在它调用exec或exit之后,父进程才可能被调度运行。
2)vfork和fork一样都创建一个子进程,但它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit),于是也就不访问该地址空间。
相反,在子进程中调用exec或exit之前,它在父进程的地址空间中运行,在exec之后子进程会有自己的进程空间
2.2.8 进程的替换
进程替换由exec家族函数完成:
1.exec函数族提供了六种在进程中启动另一个程序的方法
2.exec函数族可以根据指定的文件名或目录名找到可执行文件
3。调用exec函数的进程并不创建新的进程,故调用exec前后,进程的进程号并不会改变,其执行的程序完全由新的程序替换,而新程序则从其main函数开始执行
exec相关函数:
#include <unistd.h>
int execl(const char *pathname,const char *argv0,...,NULL);
int execlp(const char *filename,const char *arg0,...,NULL);
int execle(const char *pathname,const char *arg0,...,NULL,char *const envp[]);
int execv(const char *pathname,char *const argv[]);
int execvp(const char *filename,char *const argv[]);
int execve(const char *pathname,char *const argv[],char *const envp[]);
六个exec函数中只有execve是真正意义的系统调用(内核提供的接口),其它函数都是在此基础上经过封装的库函数
l(list):参数地址列表,以空指针结尾
v(vector):存有各参数地址的指针数组的地址,使用时先构造一个指针数组,指针数组存各参数的地址,然后将该指针数组地址作为函数的参数。
p(path):按PATH环境变量指定的目录搜索可执行文件。以p结尾的exec函数去文件名作为参数。当指定filename作为参数时,若filename中包含/
,则将其视为路径名,并直接到指定的路径中执行程序。
e(environment):存有环境变量字符串地址的指针数组的地址。execle和execve改变的是exec启动的程序的环境变量(新的环境变量完全由enviroment指定),其他四个函数启动的程序则使用默认系统环境变量。
【注意】
exec函数族与一般的函数不同,exec函数族中的函数执行成功后不会返回。只有调用失败了,它们才会返回-1.失败后从原程序的调用点接着往下执行。
如果用到了exec函数族,一定要记得加错误判断语句。
一个进程调用exec后,除了进程ID,进程还保留了下列特征不变:
父进程号、进程组号、控制终端、根目录、当前工作目录、进程信号屏蔽集、未处理信号....
2.2.9 system函数
system会调用fork函数产生子进程,子进程调用exec启动/bin/sh -c string来执行参数string字符串所代表的命令,此命令执行完后返回原调用进程
#include <stdlib.h>
int system(const char *command);
参数command:要执行的命令的字符串
返回值:
如果command 为NULL ,则system()函数返回非0,一般为1。
如果system()在调用/bin/sh 时失败则返回127,其他失败原因返回-1。
system调用成功后会返回执行shell命令后的返回值。其返回值可能为1、127也可能为-1,故最好应再检查errno来确认执行成功
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!