2 进程
约 5944 字大约 20 分钟
2025-06-15
//使用linux命令创建一个子进程,成功返回0失败返回错误编号
int system (const char *__command) __wur;
系统调用
fork复制一个子进程 pid_t定义在头文件/usr/include/x86-64_64-linux-gnu/sys/types.h中
//创建一个子进程(相当于复制,包括内存空间),返回进程号
//若在父进程中返回子进程的PID。在子进程中返回0。发生错误返回-1
__pid_t fork(void)
typedef pid_t pid;
//返回调用进程的PID,不会失败必定返回一个PID
pid_t getpid(void);
//返回调用进程的父进程PID,不会失败必然返回一个PPID
pid_t getppid(void);
//函数正常执行且休眠时间结束,则返回0,如果sleep()由于接收到信号而被提前唤醒,则返回剩余的未休眠秒数
unsigned int sleep(unsigned int seconds);
//跳转到指定的可执行文件程序中
//path:要跳转的目标可执行程序的路径(系统的相对路径或绝对路径也就是pwd输出的内容)
//argv[]:传入的参数(字符指针数组,第一个元素要是执行./目标文件的命令,可以是./main也可以是main的绝对路径,后面的参数是运行目标可执行文件要传入的参数,有几个写几个,最后一个必须是NULL)
// (1):第一个参数固定是程序的名称->执行程序的路径
// (2):执行程序需要传入的参数
// (3):最后一个参数一定是NULL
//envp[]:传递的环境变量(如果跳转目标的路径没指定清楚就需要传入echo $PATH的内容,最后的元素一定也是NULL,写明就只需要传入NULL就可以)
// (1):环境变量参数:key=value
// (2):最后一个参数一定是NULL
//return:成功没有返回值根本没办法返回下面的代码也没有意义,失败返回-1
//跳转后只有进程号保留下来,其他删除
int execve (const char *__path, char *const __argv[],char *const __envp[])
//等待任意子进程的终止并获取子进程的退出状态赋值给wstatus
pid_t wait(int* wstatus)
//父进程调用等待子进程运行完成
//pid:等待的模式
//(1)小于-1 例如-1*pgid,则等待进程组 ID 等于pgid 的所有进程终止
//(2)等于-1会等待任何子进程终止,并返回最先终止的那个子进程的进程ID儿孙都算
//(3)等于0等待同一进程组中任何子进程终止(但不包括组领导进程)->只算儿子
//(4)大于0仅等待指定进程ID的子进程终止
//wstatus:整数指针,子进程返回的状态码会保存到该int
//options:选项的值是以下常量之一或多个的按位或(OR)运算的结果;二进制对应选项,可多选:
//(1)WNOHANG等待但不挂起如果没有子进程终止,也立即返回;用于查看子进程状态而非等待
//(2)WUNTRACED收到子进程处于收到信号停止的状态,也返回。
//(3)WCONTINUED(自LinuX2.6.10起)如果通过发送SIGCONT信号恢复了个已停止的子进程,则也返回。
//return:(1)成功等到子进程停止返pid
//(2)没等到并且没有设置WNOHANG一直等
//(3)没等到设置WNOHANG返回(4)出错返回-1
pid_t waitpid(pid_t pid,int* wstatus,int options);
pidt是_pidt的别名,后者定义在/usr/include/x86-64_64-linux-gnu/bits/types.h中
__STD_TYPE __PID_T_TYPE __pid_t;
#define __PID_T_TYPE __S32_TYPE
#define __S32_TYPE int
#define __STD__TYPE typedef
STD_TYPE预处理后被替换为typedef,_PID_T_TYPE预处理后被替换为int 因此pid_t实际上就是int
fork后的代码是子进程的同时也被父进程运行,即fork后的代码运行两次。这时使用变量接收到的fork返回值就有两个值,一个是父进程运行时返回子进程的PID,一个是子进程运行时返回0(指自身)
子进程会把父进程的资源复制一份,对于打开的文件描述符会在引用计数上+1,父子进程同时引用 使用close关闭引用计数就-1
exec系列函数可以在同一进程中路跳转执行另外一个程序 execve的argv[]和envp[]参数最后一定是NULL
char* args[]={"/home/pink/project/vscode",name,NULL};
char* envs[]={"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/snap/bin",NULL};
//执行跳转
int ret=execve(args[0],args,envs);
通常采用fork和execve配合使用,创建了新进程后跳转到另一个程序中执行
- 僵死进程:父进程除了可以启动子进程外还要负责回收子进程的状态,如果子进程结束后父进程没有正常回收,那么子进程就会变成一个僵死进程——即程序执行完成但进程没有完全结束,其内核中PCB结构体没有释放
- 孤儿进程:父进程运行结束了子进程还在运行,那么这个子进程就会变为一个孤儿进程,这时子进程的回收工作就交给了父进程的父进程,即被祖先进程收养 孤儿进程会被其祖先自动领养。此时的子进程因为和终端切断了联系,所以很难再进行标准输入使其停止了,所以写代码的时候一定要注意避免出现孤儿进程。
使用ps查看的进程带[]的是内核线程,不带中括号的是进程
进程通讯
进程之前的内存是隔离的,如果多个进程之间需要进行信息交换,常用的方法有以下几种:
- Unix Domain Socket IPC套接字
- 管道(有名管道、无名管道)
- 共享内存
- 消息队列
- 信号量
管道
匿名管道
在内核空间创建管道,用于父子进程或者其他相关联的进程之间通过管道进行双向的数据传输 由于管道是单工通讯的,因此在进程间通信中要把不需要的管道端口关闭,将管道当作是文件描述符使用close关闭 管道的读写端通过打开的文件描述符来传递,因此要通信的两个进程必须从它们的公共祖先那里继承管道文件描述符。 也就是说要在创建子进程之前创建管道
//pipefd:用于返回指向管道两端的两个文件描述符。
//pipefd[0]指向管道的读端。pipefd[1]指向管道的写端
//return:成功0不成功-1,并且 pipefd 不会改变
int pipe(int pipefd[2]);
示例:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<fcntl.h>
#include<string.h>
int main(int argc,const char* argv[]){
pid_t cpid;
int pipefd[2];
//将程序传递进来的第一个命令行参数通过管道传输给子进程
if (argc!=2){
fprintf(stderr,"%s:请填写需要传递的信息\n",argv[0]);
exit(EXIT_FAILURE);
}
//创建管道
if (pipe(pipefd)==-1){
perror("创建管道失败");
exit(EXIT_FAILURE);
}
//复制父子进程
cpid=fork();
if (cpid==-1){
perror("创建子进程失败");
exit(EXIT_FAILURE);
}else if (cpid==0){
//子进程
//读取管道数据打印到控制台
close(pipefd[1]);
char* str="子进程接受信息\n";
write(STDOUT_FILENO,str,sizeof(str));
char buf;
while (read(pipefd[0],&buf,1)>0){
write(STDOUT_FILENO,&buf,1);
}
write(STDOUT_FILENO,"\n",1);
close(pipefd[0]);
_exit(EXIT_SUCCESS);
}else{
//父进程
//写入管道数据给子进程读
close(pipefd[0]);
printf("父进程%d给子进程传递信息\n",getpid());
write(pipefd[1],argv[1],strlen(argv[1]));
close(pipefd[1]);
waitpid(cpid,NULL,0);
exit(EXIT_FAILURE);
}
return 0;
}
有名管道
pipe是匿名管道,只能在有父子关系的进程间使用,某些场景下并不能满足需求。与匿名管道相对的是有名管道,在Linux中称为FIFO,即FirstInFirstOut,先进先出队列。
FIFO和Pipe一样,提供了双向进程间通信渠道。但要注意的是,无论是有名管道还是匿名管道,同一条管道只应用于单向通信,否则可能出现通信混乱(进程读到自己发的数据)。 一旦创建了fifo专用文件,任何进程都可以像操作文件一样打开fifo执行读写操作 一般是创建在系统的临时文件夹中:/tmp/
//pathname有名管道绑定的文件路径和文件名
//mode有名管道绑定文件的权限
//mode要以0+八进制的三位数来指定权限
//成功返回0,失败返回-1
int mkfifo(const char* pathname,mode_t mode)
//清除有名管道,__name就是有名管道的路径
//返回-1表示清除出错
int unlink (const char *__name)
有名管道就是一个特殊文件,创建之后是可以重复使用的,但不推荐
使用fifo交换数据时内核会在内部传递所有数据,不会将其写入文件系统,因此fifo文件始终为0,且文件类型也是特殊的p
有名管道本质上还是一个文件所以再次使用的时候如果原本创建的有名管道没有清除再创建同名的管道文件就会报错文件已存在,如果要多次使用可以不清除,报错文件已存在的错误码是17,针对这个错误码跳过即可,若需要清除就使用unlink效果和rm命令一样
共享内存
适用于比管道大得多的进程间通讯 共享内存存在于临时文件系统中 Linux的临时文件系统(tmpfs)是一种基于内存的文件系统,它将数据存储在RAM或者在需要时部分使用交换空间(swap)。tmpfs.访问速度快,但因为存储在内存,重启后数据清空,通常用于存储一些临时文件。 内存共享对象在临时文件系统中的表示位于/dev/shm目录下。
在内存中开启一块内存共享对象,可以像使用文件描述符一样使用这块内存对象
使用共享内存对象有四个步骤:
- 创建共享内存对象 在指定的路径和文件名称中创建一个共享内存,返回文件描述符
路径名称以/开头,以\0结尾的,中间不能有正斜杠(c风格字符串末尾自带\0)路径名称,打开方式,创建时创建文件的权限
int shm_open(const char* name,int oflag,mode_t mode)
- 指定内存大小 truncate和ftruncate都可以将文件缩放到指定大小,二者的行为类似:如果文件被缩小,截断部分的数据丢失,如果文件空间被放大,扩展的部分均为0字符。缩放前后文件的偏移量不会更改。缩放成功返回0,失败返回-1。 不同的是,前者需要指定路径,而后者需要提供文件描述符;ftruncate缩放的文件描述符可以是通过 shm_open开启的内存对象,而truncate 缩放的文件必须是文件系统已存在文件,若文件不存在或没有权限则会失败。
int truncate(const char *path,off_t length);
int ftruncate(int fd,off_t length);
- 内存映射 之前的创建共享内存和指定大小步骤在内存映射之前实际上都没有真正的内存与之对应,只有进行了内存映射才会将共享内存对象映射到内存地址上取,之后才可以使用它
将文件映射到内存区域
- addr:指定期望映射内存起始地址的指针,通常填NULL让系统选择合适的内存
- length:要映射的长度,以字节为单位
- port:保护标志,是以下标志的组合(使用|隔开): PROT_READ:允许读取映射区域 PROT WRITE:允许写入映射区域 PROT_EXEC:允许执行映射区域 PROT NONE:页面不可访问
- flags:映射选项标志 MAP_SHARED:映射区域是共享的,对映射区域的修改会影响文件和其他映射到同一区域的进程(一般使用共享) MAP_PRIVATE:映射区域是私有的,对映射区域的修改不会影响原始文件,对文件的修改会被暂时保存在一个私有副本中 MAP_ANONYMOUS:创建一个匿名映射,不与任何文件关联 MAP_FIXED:强制映射到指定的地址,如果不允许映射,将返回错误*int fd:文件描述符,用于指定要映射的文件或设备,如果是匿名映射,则传入无效的文件描述符 (例如-1)
- fd:文件描述符,用于指定要映射的文件或设备,如果要匿名映射则传入无效的文件描述符(如-1)
- offset:文件开头的偏移量,映射开始的位置(上次使用到哪个位置用偏移量,从偏移位置开始断点续传) 成功时,返回映射区域的起始地址,可以像操作操作普通内存那样使用这个地址进行读写。失败时返回(void*)-1,并且设置errno变量来表示错误原因
void* mmap(void* addr,size_t length, int prot, int flags,int fd,off_t offset)
映射完之后可以释放文件描述符,也就用不到了,直接使用共享内存进行通讯
[!NOTE] 直接赋值字符串到指针 由于得到的共享内存是一个字符指针,因此不可以直接赋值
shmp="baga\n";
将字符串直接赋给shmp
指针,这是不正确的。这会导致指针重新指向一个字符串常量的地址,并不会改变共享内存的内容。应该使用strcpy()
或类似函数来复制字符串内容到共享内存区域。
使用共享内存
取消映射关系 addr:指向之前通过mmap映射的内存区域的起始地址的指针,这个地址必须是有效的,并且必须是mmap返回的有效映射地址 length:这是要解除映射的内存区域的大小(以字节为单位),它必须与之前通过 mmap映射的大小一致 成功返回0,失败返回-1
int munmap(void* addr,size_t length);
- 删除共享内存对象 根据路径名删除尽管这个函数被称为unlink但它并没有真正删除共享内存本身,而是移除了与共享内存对象关联的名称,使得通过该名称无法再打开共享内存。当所有已打开该共享内存段的进程关闭它们的描述符后,系统才会真正释放共享内存资源
int shm_unlink(const char* name)
共享内存是存在于临时文件系统中的,临时文件系统是基于内存的。可以使用df -h
命令查看临时文件系统的大小,其中的\dev\shm就是 而我们创建的共享内存对象也是放在这个区域的
消息队列
Posix消息队列
<mqueue.h> 管道和共享内存的通讯方式消息的=都是一块一块的,使用起来不方便 而消息队列讲消息分为一条一条,这样使用起来就方便许多
我们可以通过设置POSIX消息队列的模式为O_RDWR,使它可以用于收发数据,从技术上讲,单条消息队列可以用于双向通信,但是这会导致消息混乱
若出现Invalid argument
报错原因可能有 : attr.mq_maxmsg 不能超过文件 /proc/sys/fs/mqueue/msg_max 中的数值
- 创建消息队列 创建或打开一个已存在的posix消息队列,消息队列是通过名称唯一标识的
name:消息队列的名称,以/开头,以\0结尾的(普通的字符串默认后面带有\0不用刻意添加),中间不能有正斜杠
oflag:指定消息队列的控制权限 O_RDONLY:打开的消息队列只用于接收消息 O_WRONLY:打开的消息队列只用于发送消息 O_CREAT : 队列不存在时创建队列 O_RDWR:打开的消息队列可以用于收发消息 O_EXCL : 与O_CREAT一起使用,若消息队列已存在,则错误返回 O_NONBLOCK : 以非阻塞模式打开如果设置了这个选项,在默认情况下收发消息发生阻塞时,会转而失败,并提示错误EAGAIN 可以与以下选项中的0至多个或操作之后作为oflag O_CLOEXEC设置close-on-exec标记,这个标记表示执行exec时关闭文件描述符
mode如果oflag使用了O_CRAET就需要给创建的消息队列指定0+八进制的权限
attr:属性信息,如果为NULL,则队列以默认属性创建
return: 成功返回消息队列描述符,失败返回-1并设置errorno
//对于没有消息队列创建时
mqd_t mq_open(const char *name, int oflag, mode_t mode,struct mq_attr* attr);
//对于读的一方写方已经创建好了消息队列使用该函数打开消息队列
mqd_t mq_open(const char* name,int oflag);
struct mq_attr消息队列的属性信息
struct mq_attr{
1ong mq_flags;阻塞标记,只能是0或O_NONBLOCK如果是mq_open打开的忽略,因为这个标记是通过前者的调用传递的
long mq_maxmsg;队列可以容纳的消息的最大数量
long mq_msgsize;单条消息的最大允许大小,以字节为单位
long mq_curmsgs;当前队列中的消息数量,对于mq_open打开的忽略
}
- 发送消息 当消息队列满的时候就等待,调用阻塞直至有充足的空间允许新的消息入队,或者达到abs_timeout指定的等待时间节点,或者调用被信号处理函数打断。 如果在mq_open时指定了O_NONBLOCK标记则转而失败,并返回错误EAGAIN
- mqdes:消息队列描述符
- msg_ptr:指向消息的指针
- msg_len:msg_ptr指向的消息长度,不能超过队列的mg_msgsize属性指定的队列最大容量,长度为0的消息是被允许的
- msg_prio:非负整数,指定了消息的优先级,消息队列中的数据是按照优先级降序排列的,如果新旧消息的优先级相同,则新的消息排在后面。
- abs_timeout:指向struct timespec类型的对象,指定了阻塞等待的最晚时间。如果消息队列已满,且abs_timeout指定的时间节点已过期,则调用立即返回。 成功返回0,失败返回-1,同时设置errno以指明错误原因。 如果消息队列满了,阻塞模式下将一直阻塞,非阻塞模式下直接返回错误。
int mq_timedsend(mqd_t mqdes,const char *msg_ptr,size_t msg_len,unsigned int msg_prio,const struct timespec* abs_timeout);
不带时间的
int mq_send(mqd_t mqdes, const char *msg_ptr,size_t msg_len, unsigned int msg_prio);
struct timespaec 时间结构体,提供了纳秒级的UNIX时间戳
struct timespec{
time_t tv_sec;//秒
long tv_nsec;//纳秒
}
时钟获取 <time.h> 获取以struct timespec形式表示的clockid指定的时钟
- clockid:特定时钟的标识符,常用的是CLOCK_REALTIME ( 0 ) ,表示当前时间的时钟
- tp:用于接收时间信息的缓存 成功返回θ,失败返回-1,同时设置 errno 以指明错误原因
int clock_gettime(clockid_t tclockid,struct timespec* tp);
- 接收消息(带时间的) 带时间的是系统调用 从消息队列中取走最早入队且权限最高的消息,将其放入msg_ptr指向的缓存中。如果消息队列为空,默认情况下调用阻塞,此时的行为与mg_timedsend同理。到达指定时间点还没有获取到就退出
- mqdes消息队列描述符
- msg_ptr接收消息的缓
- msg_len msg_ptr 指向的缓存区的大小,必须大于等于 mq_msgsize 属性指定的队列单条消息最大字节数
- msg_prio如果不为NULL,则用于接收接收到的消息的优先级
- abs_timeout阻塞时等待的最晚时间节点,同mq_timedsend 成功则返回接收到的消息的字节数,失败返回-1,并设置errno指明错误原因
ssize_t mq_timedreceive(mqd_t mqdes,char* msg_ptr,size_t msg_len,unsigned int* msg_prio,const struct timespec* abs_timeout);
不带时间的
ssize_t mq_receive(mqd_t mqdes, char *mdg_ptr,size_t msg_len, unsigned int *msg_prio);
- 关闭消息队列 关闭消息队列不再使用,并不是删除删除是`unlink' 成功返回0,失败返回-1
int mq_close(mqd_t mqdes)
- 释放消息队列 清除name对应的消息队列,mqueue文件系统中的对应文件被立即清除。消息队列本身的清除必须等待所有指向该消息队列的描述符全部关闭之后才会发生 name:消息队列名称 成功返回0,失败返回-1,并设置errno指明错误原因
int mq_unlink(const char *name);
设置消息队列 不常用 mq_setattr()能够修改的唯一特性是O_NONBLOCK标记的状态 使用newattr指向的mq_attr结构中的mq_flags字段来修改与描述符mqdes相关联的消息队列描述的标记. 如果oldattr不为NULL,那么就返回一个包含之前的消息队列描述标记和消息队列特性的mq_attr结构(即于mq_getattr()效果相同)
成功返回0,失败返回-1
int mq_setattr(mq_t mqdes, const struct mq_attr *newattr, struct mq_attr *oldattr);
System V消息队列
信号
在Linux中,信号是一种用于通知进程发生了某种事件的机制。信号可以由内核、其他进程或者通过命令行工具发送给目标进程。Linux系统中有多种信号,每种信号都用一个唯一的整数值来表示,例如,常见的信号包括:
信号 | 对应的整型值 | 作用 |
---|---|---|
SIGINT | 2 | 用户在终端按下Ctrl+C时发送给前台进程的信号,通常用于请求进程终止 |
SIGKILL | 9 | 强制终止进程的信号,它会立即终止目标进程,且不能被捕获或忽略。 |
SIGUSR1和SIGUSR2 | 10和12 | 这两个信号是用户自定义的信号,默认为空,可以由应用程序使用。 |
SIGSEGV | 11 | 表示进程非法内存访问的信号,通常是由于进程尝试访问未分配的内存或者试图执行非法指令而导致的。 |
SIGALRM | 14 | 定时器信号,通常用于在一定时间间隔后向目标进程发送信号。 |
SIGTERM | 15 | 用于请求进程终止的信号,通常由系统管理员或其他进程发送给目标进程。 |
要查看所有信号使用kill -l 命令查看 |
信号处理例程
<signal.h>
相当于重写了对应的信号处理,当收到该信号后依旧执行该信号的操作,在此之前也执行自己传递到signal函数中的函数
//信号处理函数声明
typedef void(*sighandler_t)(int);
//signal系统调用会注册某一信号对应的处理函数。如果注册成功,当进程收到这信号时,将不会调用默认的处理函数,而是调用这里的自定义函数
sighandler_t signal(int signum,sighandler_t handler);
signum:要处理的信号 handler:当收到对应的signum信号时,要调用的函数 返回之前的信号处理函数,如果错误会返回SEG_ERR
贡献者
版权所有
版权归属:PinkDopeyBug