【linux】系统编程-1-进程、管道和信号
1. 进程
1.1 概念
- 程序
- 程序是存放在存储介质上的一个可执行文件
- 进程
- 进程是程序执行的过程,是程序在执行过程中分配和管理资源的基本单位
- 程序是静态的,进程是动态的。进程的状态是变化的,其包括进程的创建、调度和消亡
- 线程
- 线程是CPU调度和分派的基本单位,它可与同属一个进程的其他的线程共享进程所拥有的全部资源
- 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程
- 进程ID
- 进程ID 是一个16位的正整数,默认取值范围是从 2 到 32768(可以修改)
- PID数字为1的值一般是为特殊进程 init 保留
- 父进程
- 任何进程(除init进程)都是由另一个进程启动,该进程称为被启动进程的父进程(ID号称为:PID),被启动的进程称为子进程(ID号称为:PPID),
- 父进程号无法在用户层修改
1.2 查看进程
- 查看进程命令
-
ps -aux
- 查看系统进程
-
pstree
- 将进程以树状关系列出来
-
1.3 启动新进程
- 介绍三种方法启动新进程
- system() 函数
- fork() 函数
- exec() 函数
1.3.1 system() 函数
- 可以理解为 启动新进程
- system()启动了一个运行着/bin/sh的子进程
- 说明 system() 函数依赖与 shell
-
int system (const char *string )
- 效果就相当于执行
sh –c string
- 效果就相当于执行
- system() 函数的特点
- 建立独立进程,拥有独立的代码空间,内存空间
- 等待新的进程执行完毕,system才返回。(阻塞)
- 例程
- system 运行完才会返回,才会在当前终端打印出数据
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
pid_t result;
printf("This is a system demo!\n\n");
/*调用 system()函数*/
result = system("ls -l");
printf("Done!\n\n");
return result;
}
1.3.2 fork() 函数
- 可以理解为 复制进程
- 头文件
#include<unistd.h>
#include<sys/types.h>
-
pid_t fork( void);
- 若成功调用一次则
- 子进程返回 0
- 父进程返回子进程 ID
- 出错返回 -1
- 若成功调用一次则
- 例程
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
pid_t result;
printf("This is a fork demo!\n\n");
/*调用 fork()函数*/
result = fork();
/*通过 result 的值来判断 fork()函数的返回情况,首先进行出错处理*/
if(result == -1) {
printf("Fork error\n");
}
/*返回值为 0 代表子进程*/
else if (result == 0) {
printf("The returned value is %d, In child process!! My PID is %d\n\n", result, getpid());
}
/*返回值大于 0 代表父进程*/
else {
printf("The returned value is %d, In father process!! My PID is %d\n\n", result, getpid());
}
return result;}
1.3.2 exce 系列函数
- 可以理解为 替换进程
- 调用 exec 并不创建新进程,所以前后的进程 ID 并未改变
- exec 只是用另一个新程序替换了当前进程的正文、数据、堆和栈段
- 在原进程中已经打开的文件描述符,在新进程中仍将保持打开,除非它们的“执行时关闭标志”(close on exec flag)被置位
- 任何在原进程中已打开的目录流都将在新进程中被关闭
- 举个例子,A进程调用 exce 系列函数启动一个进程B,此时进程B会替换进程A,进程A的内存空间、数据段、代码段等内容都将被进程B占用,进程A将不复存在
1.3.2.1 exce 系列函数说明
-
exec 系列函数有 6 个不同的 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, ..., char *const envp[])
int execv(const char *path, char *const argv[])
int execvp(const char *file, char *const argv[])
int execve(const char *path, char *const argv[], char *const envp[])
- 函数说明
- 名称包含 l 字母的函数(execl、 execlp 和execle)接收参数列表”list”作为调用程序的参数
- 名称包含 p 字母的函数(execvp 和execlp)接受一个程序名作为参数,然后在当前的执行路径中搜索并执行这个程序
- 名字不包含 p 字母的函数在调用时必须指定程序的完整路径,其实就是在系统环境变量”PATH”搜索可执行文件
- 名称包含 v 字母的函数(execv、execvp 和 execve)的命令参数通过一个数组”vector”传入
- 名称包含 e 字母的函数(execve 和 execle)比其它函数多接收一个指明环境变量列表的参数,并且可以通过参数envp传递字符串数组作为新程序的环境变量,这个envp参数的格式应为一个以 NULL 指针作为结束标记的字符串数组,每个字符串应该表示为”environment =virables”的形式
1.3 终止进程
- 可以分为 5 种进程终止
- 正常终止
- 从 main 函数返回
- 调用 exit() 终止
- 调用 _exit() 函数终止
- 异常终止
- 调用 abort() 函数终止
- 由系统信号终止
- 正常终止
1.4 等待进程
- 父进程中调用wait()或者waitpid()函数让父进程等待子进程的结束
1.4.1 wait() 函数
- wait()函数只是 waitpid() 函数的一个特例,在 Linux内部实现 wait 函数时直接调用的就是 waitpid 函数
-
pid_t wait(int *wstatus);
- wait() 函数在被调用的时候,系统将暂停父进程的执行,直到有信号来到或子进程结束
- 如果在调用 wait() 函数时子进程已经结束,则会立即返回子进程结束状态值
- 子进程的结束状态信息会由参数wstatus返回
- 该函数的返回值为子进程的PID
- 注意
- wait()要与fork()配套出现,且 fork() 调用先
- 参数wstatus用来保存被收集进程退出时的一些状态
- 可以使用以下宏来判断退出状态
- WIFEXITED(status) :如果子进程正常结束,返回一个非零值
- WEXITSTATUS(status): 如果WIFEXITED非零,返回子进程退出码
- WIFSIGNALED(status) :子进程因为捕获信号而终止,返回非零值
- WTERMSIG(status) :如果WIFSIGNALED非零,返回信号代码
- WIFSTOPPED(status): 如果子进程被暂停,返回一个非零值
- WSTOPSIG(status): 如果WIFSTOPPED非零,返回一个信号代码
1.4.2 waitpid() 函数
- wait()函数只是 waitpid() 函数的一个特例,在 Linux内部实现 wait 函数时直接调用的就是 waitpid 函数
-
pid_t waitpid(pid_t pid, int *wstatus, int options);
- pid:参数pid为要等待的子进程ID
- pid < -1:等待进程组号为pid绝对值的任何子进程
- pid = -1:等待任何子进程,此时的waitpid()函数就等同于wait()函数
- pid = 0:等待进程组号与目前进程相同的任何子进程,即等待任何与调用waitpid()函数的进程在同一个进程组的进程
- pid > 0:等待指定进程号为pid的子进程
- wstatus:与wait()函数一样
- options:参数 options 提供了一些另外的选项来控制waitpid()函数的行为。如果不想使用这些选项,则可以把这个参数设为0
- pid:参数pid为要等待的子进程ID
2. 管道
2.1 概念
- 管道
- 管道是 Linux 由 Unix 那里继承过来的进程间的通信机制,它是Unix早期的一个重要通信机制。
- 其思想是,在内存中创建一个共享文件,从而使通信双方利用这个共享文件来传递信息。由于这种方式具有单向传递数据的特点,所以这个作为传递消息的共享文件就叫做“管道”
- 管道分类
- 匿名管道(无名管道)(PIPE)
- 命名管道(有名管道)(FIFO)
2.2 匿名管道
2.2.1 匿名管道特征
- 没有名字,因此不能使用 open() 函数打开,但可以使用 close() 函数关闭
- 只提供单向通信
- 只能用于具有血缘关系的进程间通信,通常用于父子进程建通信
- 管道是基于字节流来通信的
- 依赖于文件系统,它的生命周期随进程的结束而结束
- 写入操作不具有原子性,因此只能用于一对一的简单通信情形
- 管道也可以看成是一种特殊的文件,对于它的读写也可以使用普通的read()和write()等函数。但是它又不是普通的文件,并不属于其他任何文件系统,并且只存在于内核的内存空间中,因此不能使用lseek()来定位
2.2.2 pipe() 函数
- pipe() 函数用于创建一个匿名管道,一个可用于进程间通信的单向数据通道。
- 头文件
#include <unistd.h>
- 函数原型
-
int pipe(int pipefd[2]);
- pipefd[0] 指向管道的 读取 端
- pipefd[1] 指向管道的 写 端
- 返回 0:匿名管道创建成功
- 返回 -1:创建失败
-
- 使用步骤
- 父进程调用 pipe() 函数创建匿名管道
- 父进程调用 fork() 函数启动(创建)一个子进程
- 若想从父进程将数据传递给子进程
- 父进程:关闭读取端
- 子进程:关闭写端
- 若想从子进程将数据传递给父进程
- 父进程:关闭写端
- 子进程:关闭读取端
- 当不需要使用管道时,关闭所有端口即可
- 例程
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_DATA_LEN 256
#define DELAY_TIME 1
int main()
{
pid_t pid;
int pipe_fd[2]; //(1)
char buf[MAX_DATA_LEN];
const char data[] = "Pipe Test Program";
int real_read, real_write;
memset((void*)buf, 0, sizeof(buf));
/* 创建管道 */
if (pipe(pipe_fd) < 0) //(2)
{
printf("pipe create error\n");
exit(1);
}
/* 创建一子进程 */
if ((pid = fork()) == 0)
{
/* 子进程关闭写描述符,并通过使子进程暂停 3s 等待父进程已关闭相应的读描述符 */
close(pipe_fd[1]);
sleep(DELAY_TIME * 3);
/* 子进程读取管道内容 */
if ((real_read = read(pipe_fd[0], buf, MAX_DATA_LEN)) > 0)
{
printf("%d bytes read from the pipe is '%s'\n", real_read, buf);
}
/* 关闭子进程读描述符 */
close(pipe_fd[0]);
exit(0);
}
else if (pid > 0)
{
/* 父进程关闭读描述符,并通过使父进程暂停 1s 等待子进程已关闭相应的写描述符 */
close(pipe_fd[0]);
sleep(DELAY_TIME);
if((real_write = write(pipe_fd[1], data, strlen(data))) != -1)
{
printf("Parent write %d bytes : '%s'\n", real_write, data);
}
/*关闭父进程写描述符*/
close(pipe_fd[1]);
/*收集子进程退出信息*/
waitpid(pid, NULL, 0);
exit(0);
}
}
2.3 命名管道
2.3.1 命名管道特征
- 有名字,存储于普通文件系统之中
- 任何具有相应权限的进程都可以使用 open() 来获取命名管道的文件描述符
- 跟普通文件一样:使用统一的 read()/write() 来读写
- 跟普通文件不同:不能使用 lseek() 来定位,原因是数据存储于内存中
- 具有写入原子性,支持多写者同时进行写操作而数据不会互相践踏
- 遵循先进先出(First In First Out)原则,最先被写入 FIFO 的数据,最先被读出来
2.3.2 创建命名管道命令
-
mkfifo
- 如
mkfifo test
- test 文件为命名管道文件
- 如
2.3.3 fifo() 函数
- fifo() 函数
- 头文件
#include <unistd.h>
- 函数原型
-
int mkfifo(const char * pathname,mode_t mode);
- pathname:命名管道文件
- mode:
- O_RDONLY:读管道
- O_WRONLY:写管道
- O_RDWR:读写管道
- O_NONBLOCK:非阻塞
- O_CREAT:如果该文件不存在,那么就创建一个新的文件,并用第三个参数为其设置权限
- O_EXCL:如果使用 O_CREAT 时文件存在,那么可返回错误消息
- 返回值:
- 0:成功
- EACCESS:参数 filename 所指定的目录路径无可执行的权限
- EEXIST:参数 filename 所指定的文件已存在
- ENAMETOOLONG:参数 filename 的路径名称太长
- ENOENT:参数 filename 包含的目录不存在
- ENOSPC:文件系统的剩余空间不足
- ENOTDIR:参数 filename 路径中的目录存在但却非真正的目录
- EROFS:参数 filename 指定的文件存在于只读文件系统内
-
- 例程
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <string.h>
#define MYFIFO "myfifo" /* 命名管道文件名*/
#define MAX_BUFFER_SIZE PIPE_BUF /* 4096 定义在于 limits.h 中*/
void fifo_read(void)
{
char buff[MAX_BUFFER_SIZE];
int fd;
int nread;
printf("***************** read fifo ************************\n");
/* 判断命名管道是否已存在,若尚未创建,则以相应的权限创建*/
if (access(MYFIFO, F_OK) == -1)
{
if ((mkfifo(MYFIFO, 0666) < 0) && (errno != EEXIST))
{
printf("Cannot create fifo file\n");
exit(1);
}
}
/* 以只读阻塞方式打开命名管道 */
fd = open(MYFIFO, O_RDONLY);
if (fd == -1)
{
printf("Open fifo file error\n");
exit(1);
}
memset(buff, 0, sizeof(buff));
if ((nread = read(fd, buff, MAX_BUFFER_SIZE)) > 0)
{
printf("Read '%s' from FIFO\n", buff);
}
printf("***************** close fifo ************************\n");
close(fd);
exit(0);
}
void fifo_write(void)
{
int fd;
char buff[] = "this is a fifo test demo";
int nwrite;
sleep(2); //等待子进程先运行
/* 以只写阻塞方式打开 FIFO 管道 */
fd = open(MYFIFO, O_WRONLY | O_CREAT, 0644);
if (fd == -1)
{
printf("Open fifo file error\n");
exit(1);
}
printf("Write '%s' to FIFO\n", buff);
/*向管道中写入字符串*/
nwrite = write(fd, buff, MAX_BUFFER_SIZE);
if(wait(NULL)) //等待子进程退出
{
close(fd);
exit(0);
}
}
int main()
{
pid_t result;
/*调用 fork()函数*/
result = fork();
/*通过 result 的值来判断 fork() 函数的返回情况,首先进行出错处理*/
if(result == -1)
{
printf("Fork error\n");
}
else if (result == 0) /*返回值为 0 代表子进程*/
{
fifo_read();
}
else /*返回值大于 0 代表父进程*/
{
fifo_write();
}
return result;
}
3. 信号
3.1 概念及特征
- 信号(signal)
- 又称为软中断信号,用于通知进程发生了异步事件
- 它是Linux系统响应某些条件而产生的一个事件
- 它是在软件层次上对中断机制的一种模拟
- 是一种异步通信方式
- 在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的
- 信号是进程间通信机制中唯一的异步通信机制
- 信号产生
- 信号可能是由于系统中某些错误而产生
- 也可以是某个进程主动生成的一个信号
3.2 系统支持的信号
- 查询系统支持的信号种类命令:
kill -l
- linux支持62种信号(没有 32 号和 33 号信号)
- 非实时信号(不可靠):1-32
- 没有排队功能,信号可能被丢弃
- 不会立即执行
- 先放入该进程控制块(PCB),待合适的时候处理
- 实时信号(可靠信号):34-64
- 有排队功能
- 非实时信号(不可靠):1-32
3.3 信号处理
- 信号类似可分为三大类型:程序错误、外部事件以及显式请求
- 当信号发生时,信号可以采取如下三种操作:
- 忽略信号(SIGTOP 和 SIGKILL 是绝不能被忽略的)
- 捕获信号
- 让默认信号起作用
- 终止进程并且生成内存转储文件
- 终止终止进程但不生成core文件
- 忽略信号
- 暂停进程
- 若进程是暂时暂停,恢复进程,否则将忽略信号
3.4 发送信号函数
- kill()
- raise()
- alarm()
3.4.1 kill()
- 命令:
kill [信号或选项] PID(s)
- 函数
- 头文件:
#include <sys/types.h> #include <signal.h>
- 函数原型:
int kill(pid_t pid, int sig);
- pid 取值如下
- pid > 1:将信号sig发送到进程ID值为pid指定的进程
- pid = 0:信号被发送到所有和当前进程在同一个进程组的进程
- pid = -1:将sig发送到系统中所有的进程,但进程1(init)除外
- pid < -1:将信号sig发送给进程组号为-pid (pid绝对值)的每一个进程
- sig 为 信号值
- 返回值
- 0:发送成功
- -1:发送失败
- pid 取值如下
- 头文件:
3.4.2 raise()
- raise() 函数为进程向自身发送信号
- 函数
- 头文件
#include <signal.h>
- 函数原型:
int raise(int sig);
- sig 为 信号值
- 返回值
- 0:发送成功
- -1:发送失败
- 头文件
3.4.3 alarm()
- alarm() 称为闹钟函数,设置时间为 seconds 秒,时间到后,它就向进程发送SIGALARM信号。在时间未到时便重新调用 alarm() 函数,会更新到时值。
- 函数
- 头文件
#include <unistd.h>
- 函数原型:
unsigned int alarm(unsigned int seconds);
- 头文件
3.5 捕获信号函数
- signal()、sigaction()等函数
3.5.1 signal()
- signal()主要是用于捕获信号,可以改变进程中对信号的默认行为
- 函数
- 头文件
#include <signal.h>
- 函数原型
typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
- signum 是指定捕获的信号,如果指定的是一个无效的信号,或者尝试处理的信号是不可捕获或不可忽略的信号(如SIGKILL),errno将被设置为EINVAL
-
handler 是一个函数指针,它的类型是
void(*sighandler_t)(int)
类型 -
handler 也可以是一个宏定义
- SIG_IGN:忽略该信号
- SIG_DFL:采用系统默认方式处理信号
- 头文件
3.5.2 sigaction() *
- 不推荐读者使用signal(),而推荐使用
sigaction();
- 函数
- 头文件
#include <signal.h>
- 函数原型:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
-
signum:指定捕获的信号值
-
act:是一个结构体
- sa_handler 是一个函数指针,是捕获信号后的处理函数
- sa_sigaction 是扩展信号处理函数,它也是一个函数指针,不仅可以接收到int 型的信号值,还会接收到一个 siginfo_t 类 型的结构体指针,还有一个void类型的指针,还有需要注意的就是,不要同时使用 sa_handler 和 sa_sigaction,因为这两个处理函数是有联合的部分(联合体)
- sa_mask 是信号掩码,它指定了在执行信号处理函数期间阻塞的信号的掩码,被设置在该掩码中的信号,在进程响应信号期间被临时阻塞。除非使用 SA_NODEFER 标志,否则即使是当前正在处理的响应的信号再次到来的时候也会被阻塞
- re_restorer 则是一个已经废弃的成员变量,不要使用
- oldact 返回原有的信号处理参数,一般设置为NULL即可
-
sa_flags 是指定一系列用于修改信号处理过程行为的标志
- SA_NOCLDSTOP 使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号。即当它们接收到SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU(停止)中的一种时或接收到SIGCONT(恢复)时,父进程不会收到通知
- SA_NOCLDWAIT 从Linux 2.6开始就存在这个标志了,它表示父进程在它的子进程终止时不会收到 SIGCHLD 信号,这时子进程终止则不会成为僵尸进程。
- SA_NODEFER 一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置了 SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号
- SA_RESETHAND 信号处理之后重新设置为默认的处理方式。
- SA_SIGINFO 从Linux 2.2开始就存在这个标志了,使用 sa_sigaction成员而不是使用sa_handler 成员作为信号处理函数。
struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); };
- siginfo_t
siginfo_t { int si_signo; /* 信号数值 */ int si_errno; /* 错误值 */ int si_code; /* 信号代码 */ int si_trapno; /*导致硬件生成信号的陷阱号,在大多数体系结构中未使用*/ pid_t si_pid; /* 发送信号的进程ID */ uid_t si_uid; /*发送信号的真实用户ID */ int si_status; /* 退出值或信号状态*/ clock_t si_utime; /*消耗的用户时间*/ clock_t si_stime; /*消耗的系统时间*/ sigval_t si_value; /*信号值*/ int si_int; /* POSIX.1b 信号*/ void *si_ptr; int si_overrun; /*计时器溢出计数*/ int si_timerid; /* 计时器ID */ void *si_addr; /*导致故障的内存位置 */ long si_band; int si_fd; /* 文件描述符*/ short si_addr_lsb; /*地址的最低有效位 (从Linux 2.6.32开始存在) */ void *si_lower; /*地址冲突时的下限*/ void *si_upper; /*地址冲突时的上限 (从Linux 3.19开始存在) */ int si_pkey; /*导致的PTE上的保护密钥*/ void *si_call_addr; /*系统调用指令的地址*/ int si_syscall; /*尝试的系统调用次数*/ unsigned int si_arch; /* 尝试的系统调用的体系结构*/ }
-
- 头文件
3.6 信号集
- 数据类型 sigset_t 是信号集,信号掩码就是这种类型
- 头文件:
#include <signal.h>
- 函数
-
int sigemptyset(sigset_t *set);
- 将信号集初始化为空,使进程不会屏蔽任何信号
-
int sigfillset(sigset_t *set);
- 将信号集初始化为包含所有已定义的信号
-
int sigaddset(sigset_t *set, int signum);
- 添加一个信号到信号集中
-
int sigdelset(sigset_t *set, int signum);
- 从信号集中删除一个信号
-
int sigismember(const sigset_t *set, int signum);
- 判断一个信号是否在信号集中
-
- 注意:
- 一个应用程序,在使用信号集前,必须对其进行初始化,即是调用 sigemptyset() 或 sigfillset()
3.7 例子
- 例程来自野火
- 实验现象
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
/** 信号处理函数 **/void signal_handler(int sig) //(1){
printf("\nthis signal numble is %d \n",sig);
if (sig == SIGINT) {
printf("I have get SIGINT!\n\n");
printf("The signal is automatically restored to the default handler!\n\n");
/** 信号自动恢复为默认处理函数 **/
}
}
int main(void){
struct sigaction act;
printf("this is sigaction function test demo!\n\n");
/** 设置信号处理的回调函数 */
act.sa_handler = signal_handler;
/* 清空屏蔽信号集 */
sigemptyset(&act.sa_mask);
/** 在处理完信号后恢复默认信号处理 */
act.sa_flags = SA_RESETHAND;
sigaction(SIGINT, &act, NULL);
while (1)
{
printf("waiting for the SIGINT signal , please enter \"ctrl + c\"...\n\n");
sleep(1);
}
exit(0);
}
参考:
* 野火