linux-c进程笔记
进程状态
三态模型
就绪态 | 运行态 | 阻塞态 |
五态模型
状态名称 | 含义 |
---|---|
新建态 | 进程刚被创建,尚未加入就绪队列 |
就绪态 | 具备运行条件,等待系统分配资源后运行 |
运行态 | 具备运行条件,正在运行 |
阻塞态 | 不具备运行条件,等待运行 |
终止态 | 进程完成任务到达正常结束点,或出现错误或异常而被终止 |
进程创建
导入头文件
sys/type.h
和unistd.h
,使用fork()
函数
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
void test() {
for(int i = 0; i < 5; i++) {
std::cout << i << std::endl;
sleep(1);
}
}
void fork_test() {
// 创建新进程
// fork函数返回值会返回两次,一次在父进程中,一次在子进程中,在父进程中返回子进程的ID,在子进程中返回0
// 父进程中返回-1表示创建失败
pid_t pid = fork();
// 判断是父进程还是子进程
if(pid > 0) {
std::cout << "Parent process, pid: " << getpid() << ", ppid: " << getppid() << std::endl;
} else if (pid == 0) {
std::cout << "Clild process, pid: " << getpid() << ", ppid: " << getppid() << std::endl;
} else {
std::cout << "create process error" << std::endl;
}
test();
}
int main() {
fork_test();
return 0;
}
fork函数在调用时会将父进程的虚拟地址空间进行复制,采取读时共享,写时复制的策略
exec函数族
使用exec函数后,会替换剩余的代码
execl | 执行指定的可执行文件 |
execlp | 执行环境变量中的指定可执行文件 |
execv | 在给出的目录列表中寻找指定的可执行文件并执行 |
void exec_test() {
pid_t pid = fork();
if(pid > 0) {
std::cout << "Parent process, pid: " << getpid() << ", ppid: " << getppid() << std::endl;
} else if (pid == 0) {
std::cout << "Clild process, pid: " << getpid() << ", ppid: " << getppid() << std::endl;
// 只有出错才有返回值
execl("test", "test", NULL);
} else {
std::cout << "create process error" << std::endl;
}
test();
}
int main() {
using namespace std;
cout << "test" << endl;
exec_test();
return 0;
}
进程控制
进程退出
void exit(int status) | 调用前会刷新I/O缓冲区 |
void _exit(int status) | 不会刷新缓冲区 |
孤儿进程
父进程运行结束,子进程依然在进程,则子进程被成为孤儿进程
每当出现孤儿进程时,内核会把孤儿进程的父进程设置为init,在孤儿进程运行结束时,init会回收其资源
僵尸进程
进程终止时,父进程尚未回收,子进程残留资源(PCB)在内核中,变成僵尸进程
僵尸进程无法被
kill -9
杀死父进程使用
wait()
或waitpid()
释放子进程的残留资源
wait() | 阻塞 |
waitpd() | 可以设置不阻塞,可设置回收指定子进程 |
进程间通信(IPC)
同一主机进程间通信
unix进程间通信方式
匿名(无名)管道
有名管道
信号
System V进程间通信方式/POSix进程间通信方式
消息队列
共享内存
信号量
不同主机进程间通信
socket
匿名管道
UNIX系统IPC最古老的方式
# 创建ls和wc两个进程,将ls的输出作为wc的输入
ls | wc -l
匿名管道本质时内核内存中维护的缓冲器
匿名管道拥有文件的特质,可以进行读操作和写操作,但没有文件实体,有名管道有文件实体,但不存储数据。可以使用操作文件的方式对管道进行操作
管道是一个字节流,使用管道时不存在消息或消息边界的概念,从管道数据的进程可以读取任意大小的数据块,而不管管道中数据块的大小时多少
通过管道的数据时顺序的,先进先出,传递方向是单向的,一端写入一端写出,为半双工通信
管道中的数据是一次性的,一旦读取则被抛弃,无法使用
lseek()
来随机访问数据匿名管道是能在具有公共祖先的进程间使用
使用
pipe()
函数
读管道:
管道中有数据,read返回实际读到的字节数
管道中无数据:
写端被全部关闭,read返回0(文件末尾)
写端没有完全关闭,read阻塞等待
写管道:
管道读端全部关闭,进程异常终止(进程收到SIGPIPE信号)
管道读端没有全部关闭:
管道已满,write阻塞
管道没有满,write将数据写入,并返回实际写入的字节数
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <wait.h>
#include <string.h>
void pipe_test() {
int pipefd[2];
// pipefd[0]为读端,pipefd[1]为写端
int ret = pipe(pipefd);
if(ret == -1) {
perror("pipe");
exit(0);
}
pid_t pid = fork();
if(pid > 0) {
char buf[1024] = {0};
int len = read(pipefd[0], buf, sizeof(buf));
printf("chind recv: %s pid: %d\n", buf, getpid());
memset(buf, 0, 1024);
char * s = "Hello, child, I am parent.";
write(pipefd[1], s, strlen(s));
wait(NULL);
} else if(pid == 0) {
char * s = "Hello, I am child.";
write(pipefd[1], s, strlen(s));
sleep(1);
char buf[1024] = {0};
int len = read(pipefd[0], buf, sizeof(buf));
printf("parent recv: %s pid: %d\n", buf, getpid());
memset(buf, 0, 1024);
} else {
perror("fork");
}
}
int main() {
pipe_test();
return 0;
}
有名管道
以FIFO的文件形式存在于文件系统中,类型为管道文件,只要可以访问文件路径,就可以通过FIFO通信
一旦打开了FIFO就能在它上面使用与操作匿名管道和其他文件系统调用一样的I/O系统(
read()
,write()
,close()
)。FIFO和匿名管道一样分输入端和输出端,先进先出
FIFO有文件实体,但是没有数据放入
# 创建有名管道
mkfifo [name]
使用
mkfifo()
函数创建有名管道
一个为只读而打开一个管道的进程会阻塞,知道另一个进程为只写打开管道
一个为只写而打开一个管道的进程会阻塞,知道另一个进程为只读打开管道
读管道:
管道中有数据,read返回实际读到的字节数
管道中无数据:
写端被全部关闭,read返回0(文件末尾)
写端没有完全关闭,read阻塞等待
写管道:
管道读端全部关闭,进程异常终止(进程收到SIGPIPE信号)
管道读端没有全部关闭:
管道已满,write阻塞
管道没有满,write将数据写入,并返回实际写入的字节数
/**
* fifo_write.c
**/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <wait.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
void writes() {
int fd = open("fifo1", O_WRONLY);
if(fd == -1) {
perror("open");
exit(0);
}
for(int i = 0; i < 10; i++) {
char buf[1024] = {0};
sprintf(buf, "hello, %d\n", i);
printf("write data: %s", buf);
write(fd, buf, strlen(buf));
sleep(1);
}
}
void fifo_tes() {
if(access("fifo1", F_OK) == -1) {
printf("管道不存在。\n");
int ret = mkfifo("fifo1", 0664);
if(ret < 0) {
perror("mkfifo");
exit(0);
}
}
}
int main() {
fifo_tes();
writes();
return 0;
}
/**
* fifo_read.c
**/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <wait.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
void reads() {
int fd = open("fifo1", O_RDONLY);
if(fd == -1) {
perror("open");
exit(0);
}
while(1) {
char buf[1024] = {0};
int len = read(fd, buf, sizeof(buf));
if(len == 0) {
printf("写端连接断开。\n");
exit(0);
}
printf("read data: %s", buf);
sleep(1);
}
}
void fifo_tes() {
if(access("fifo1", F_OK) == -1) {
printf("管道不存在。\n");
int ret = mkfifo("fifo1", 0664);
if(ret < 0) {
perror("mkfifo");
exit(0);
}
}
}
int main() {
fifo_tes();
reads();
return 0;
}
内存映射
将文件数据映射到内存中
使用mmap()
函数开启映射,munmap()
关闭映射
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
void mmap_test() {
int fd = open("test.txt", O_RDWR);
int size = lseek(fd, 0, SEEK_END);
// 创建内存映射区
void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(ptr == NULL) {
perror("mmap");
exit(0);
}
pid_t pid = fork();
if(pid > 0) {
strcpy((char*)ptr, "hello, child!\n");
} else if(pid == 0) {
char buf[64];
strcpy(buf, (char*)ptr);
printf("read data: %s", buf);
} else {
perror("fork");
exit(0);
}
munmap(ptr, size);
}
int main() {
mmap_test();
return 0;
}
注意事项
当
mmap
权限和open
权限不匹配时,产生MAP_FIELD
错误文件偏移量必须是4k的整数倍
mmap调用失败的情况
- 第二个参数
length=0
- 第三个参数
prot
只指定了写权限可以
open
时使用O_CREAT
创建新文件,但是文件大小不能为零
mmap
后关闭文件描述符,对mmap
映射不会产生影响指针
ptr
越界产生段错误
内存映射可以实现文件复制
void mmap_anonymous() {
//匿名映射,不需要文件实体,只能进行父子间通信
int length = 4096;
void *ptr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if(ptr == MAP_FAILED) {
perror("mmap");
}
pid_t pid = fork();
if(pid > 0) {
strcpy((char*)ptr, "hello, child!\n");
wait(NULL);
} else if(pid == 0) {
sleep(1);
char buf[64];
strcpy(buf, (char*)ptr);
printf("read data: %s", buf);
} else {
perror("fork");
exit(0);
}
munmap(ptr, length);
}
信号
信号时事件发生时对进程的通知机制,有时也被称为软件中断,是再软件参差上对中断机制的一种模拟,是一种异步通信方式。
信号源于内核
对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号,如
ctrl+C
,通常会给进程发送中断信号硬件发生异常,硬件检测到了一个错误条件并通知内核,随机再由内核发送相应信号给相关进程
系统状态变化
运行
kill
命令或调用kill()
函数
使用信号的主要目的
- 让进程知道已经发生了一个特定的事
- 强迫进程执行他自己代码中的信号处理程序
信号的特点
- 简单
- 不能携带大量信息
- 满足某个特定条件才发送
- 优先级较高
查看系统定义的信号列表:
kill -l
前31个为常规信号,其余为实时信号
信号的5种默认处理动作
名称 | 含义 |
---|---|
Term | 终止进程 |
Ign | 当前进程忽略掉这个信号 |
Core | 终止进程,并生成一个Core文件 |
Stop | 暂停当前进程 |
Cont | 继续执行当前被暂停的进程 |
信号的三种状态:产生,未决,递达
信号函数
kill() | 给指定进程或进程组发送指定信号 |
raise() | 给当前进程发送指定信号 |
abort() | 杀死指定进程,相当于发送SIGABRT 信号 |
alarm() | 计时n秒后给当前线程发送信号SIGALRM ,默认终止进程 |
setitimer() | 设置定时器,微妙级别精度,可以实现周期性定时 |
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
void kill_test() {
// 给任何进程或进程组pid发送任何信号sig
// pid > 0:将信号发送给指定的进程
// pid = 0:将信号发送给当前进程组
// pid = -1:将信号发送给每一个由权限接受这个信号的进程
// pid < -1: 发送给指定进程组,pid取相反数
pid_t pid = fork();
if(pid > 0) {
int i = 0;
for(int i = 0; i < 5; i++) {
printf("child process\n");
sleep(1);
}
} else if(pid == 0) {
printf("parent process\n");
sleep(2);
printf("kill child process\n");
int ret = kill(pid, SIGINT);
}
}
int main () {
kill_test();
return 0;
}
信号捕捉
signal() | 捕捉指定信号并设置处理方式 |
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/stat.h>
#include <signal.h>
#include <sys/time.h>
void myalarm(int num) {
printf("捕捉到信号: %d\n", num);
}
void signal_test() {
// 捕捉信号
// signum 被捕捉的信号类型
// handler: 被捕捉的信号的处理方式
// - SIG_IGN: 忽略信号
// - SIG_DEF:默认处理方式
// - 回调函数:自定义处理方式,使用函数指针,由内核调用
// 返回值: 上一次注册的信号处理函数的地址,第一次返回NULL,失败返回SIG_ERR
signal(SIGALRM, myalarm);
}
void setitimer_test() {
struct itimerval new_value;
// 设置间隔时间
new_value.it_interval.tv_sec = 2;
new_value.it_interval.tv_usec = 0;
// 设置延迟时间,3秒后开始第一次定时
new_value.it_value.tv_sec = 3;
new_value.it_value.tv_usec = 0;
// 非阻塞定时器
int ret = setitimer(ITIMER_REAL, &new_value, NULL);
printf("开始定时\n");
if(ret == -1) {
perror("setitimer");
exit(0);
}
getchar();
}
int main () {
signal_test();
setitimer_test();
return 0;
}
信号集
多个信号的组合,许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为信号集的数据结构来表示,其数据类型为
sigset_t
阻塞信号集:阻塞是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生,可以手动设置
未决信号集:未决是一种状态,指的是信号从产生到信号被处理前的这一段时间,不能手动设置
sigemptyset() | 清空信号集中的数据,将信号集中所有的标志位设置为0 |
sigfillset() | 将信号集中所有的标志位设置为1 |
sigaddset() | 设置信号集中指定的信号标志位为1,设置为阻塞 |
sigdelset() | 设置信号集中指定的信号标志位为0 |
sigismemberset() | 判断某个信号是否为阻塞 |
sigprocmask() | 获取/设置内核中的阻塞信号集 |
sigpending() | 获取内核中的未定信号集 |
sigaction() | 信号捕捉,检查或改变信号的处理 |
SIGCHLD信号
SIGCHLD
信号产生的条件,子进程给父进程发送SIGCHLD
,父进程默认忽略
- 子进程终止时
- 子进程接收到
SIGSTOP
信号停止时- 子进程处在停止态,接收到
SIGCONT
后唤醒时捕捉
SIGCHLD
信号可用于处理僵尸进程
/*
SIGCHLD信号产生的3个条件:
1.子进程结束
2.子进程暂停了
3.子进程继续运行
都会给父进程发送该信号,父进程默认忽略该信号。
使用SIGCHLD信号解决僵尸进程的问题。
*/
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
#include <sys/wait.h>
void myFun(int num) {
printf("捕捉到的信号: %d\n", num);
// 回收子进程PCB的资源
// while(1) {
// wait(NULL);
// }
while(1) {
int ret = waitpid(-1, NULL, WNOHANG);
if(ret > 0) {
printf("child die , pid = %d\n", ret);
} else if(ret == 0) {
// 说明还有子进程活着
break;
} else if(ret == -1) {
// 没有子进程
break;
}
}
}
int main() {
// 提前设置好阻塞信号集,阻塞SIGCHLD,因为有可能子进程很快结束,父进程还没有注册完信号捕捉
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGCHLD);
sigprocmask(SIG_BLOCK, &set, NULL);
// 创建一些子进程
pid_t pid;
for(int i = 0; i < 20; i++) {
pid = fork();
if(pid == 0) {
break;
}
}
if(pid > 0) {
// 父进程
// 捕捉子进程死亡时发送的SIGCHLD信号
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = myFun;
sigemptyset(&act.sa_mask);
sigaction(SIGCHLD, &act, NULL);
// 注册完信号捕捉以后,解除阻塞
sigprocmask(SIG_UNBLOCK, &set, NULL);
while(1) {
printf("parent process pid : %d\n", getpid());
sleep(2);
}
} else if( pid == 0) {
// 子进程
printf("child process pid : %d\n", getpid());
}
return 0;
}
共享内存
共享内存允许多个进程共享物理内存的同一块区域(段)
共享内存为进程用户空间的一部分,所以无需内核介入
shmget() | 使用创建新的共享内存段或获取已有的共享内存段标识符 |
shmat() | 是共享内存段成为调用进程的虚拟内存的一部分,返回该内存区的地址 |
shmdt() | 分离共享内存段,进程终止时自动完成这一步 |
shctl() | 删除共享内存段,只有当所有进程都与之分离后才能销毁 |
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
// - 功能:创建一个新的共享内存段,或者获取一个既有的共享内存段的标识。
// 新创建的内存段中的数据都会被初始化为0
// - 参数:
// - key : key_t类型是一个整形,通过这个找到或者创建一个共享内存。
// 一般使用16进制表示,非0值
// - size: 共享内存的大小
// - shmflg: 属性
// - 访问权限
// - 附加属性:创建/判断共享内存是不是存在
// - 创建:IPC_CREAT
// - 判断共享内存是否存在: IPC_EXCL , 需要和IPC_CREAT一起使用
// IPC_CREAT | IPC_EXCL | 0664
// - 返回值:
// 失败:-1 并设置错误号
// 成功:>0 返回共享内存的引用的ID,后面操作共享内存都是通过这个值。
void *shmat(int shmid, const void *shmaddr, int shmflg);
// - 功能:和当前的进程进行关联
// - 参数:
// - shmid : 共享内存的标识(ID),由shmget返回值获取
// - shmaddr: 申请的共享内存的起始地址,指定NULL,内核指定
// - shmflg : 对共享内存的操作
// - 读 : SHM_RDONLY, 必须要有读权限
// - 读写: 0
// - 返回值:
// 成功:返回共享内存的首(起始)地址。 失败(void *) -1
int shmdt(const void *shmaddr);
// - 功能:解除当前进程和共享内存的关联
// - 参数:
// shmaddr:共享内存的首地址
// - 返回值:成功 0, 失败 -1
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
// - 功能:对共享内存进行操作。删除共享内存,共享内存要删除才会消失,创建共享内存的进行被销毁了对共享内存是没有任何影响。
// - 参数:
// - shmid: 共享内存的ID
// - cmd : 要做的操作
// - IPC_STAT : 获取共享内存的当前的状态
// - IPC_SET : 设置共享内存的状态
// - IPC_RMID: 标记共享内存被销毁
// - buf:需要设置或者获取的共享内存的属性信息
// - IPC_STAT : buf存储数据
// - IPC_SET : buf中需要初始化数据,设置到内核中
// - IPC_RMID : 没有用,NULL
key_t ftok(const char *pathname, int proj_id);
// - 功能:根据指定的路径名,和int值,生成一个共享内存的key
// - 参数:
// - pathname:指定一个存在的路径
// /home/nowcoder/Linux/a.txt
// /
// - proj_id: int类型的值,但是这系统调用只会使用其中的1个字节
// 范围 : 0-255 一般指定一个字符 'a'
使用
ipcs -m
查看共享内存信息使用
ipcsrm -M [key]/-m [shmid]
删除共享内存
守护进程
守护进程又称Daemon进程,是Linux中后台的服务进程,独立于控制终端且周期性执行某种任务会等待处理某些发生的事件,一般以d结尾
守护进程在系统启动时被创建并一直运行至系统关闭
守护进程不拥有控制终端,不会收到任何控制信号和终端相关的信号
/*
写一个守护进程,每隔2s获取一下系统时间,将这个时间写入到磁盘文件中。
*/
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/time.h>
#include <signal.h>
#include <time.h>
#include <stdlib.h>
#include <string.h>
void work(int num) {
// 捕捉到信号之后,获取系统时间,写入磁盘文件
time_t tm = time(NULL);
struct tm * loc = localtime(&tm);
// char buf[1024];
// sprintf(buf, "%d-%d-%d %d:%d:%d\n",loc->tm_year,loc->tm_mon
// ,loc->tm_mday, loc->tm_hour, loc->tm_min, loc->tm_sec);
// printf("%s\n", buf);
char * str = asctime(loc);
int fd = open("time.txt", O_RDWR | O_CREAT | O_APPEND, 0664);
write(fd ,str, strlen(str));
close(fd);
}
int main() {
// 1.创建子进程,退出父进程
pid_t pid = fork();
if(pid > 0) {
exit(0);
}
// 2.将子进程重新创建一个会话
setsid();
// 3.设置掩码,确保拥有所需权限
umask(022);
// 4.更改工作目录
chdir("/home/nowcoder/");
// 5. 关闭、重定向文件描述符
int fd = open("/dev/null", O_RDWR);
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
// 6.业务逻辑
// 捕捉定时信号
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = work;
sigemptyset(&act.sa_mask);
sigaction(SIGALRM, &act, NULL);
struct itimerval val;
val.it_value.tv_sec = 2;
val.it_value.tv_usec = 0;
val.it_interval.tv_sec = 2;
val.it_interval.tv_usec = 0;
// 创建定时器
setitimer(ITIMER_REAL, &val, NULL);
// 不让进程结束
while(1) {
sleep(10);
}
return 0;
}