system函数:
a. #include
b. 可以使用该函数,根据传入的命令启动一个进程
c. 返回值:成功返回0 不支持shell返回-1 失败返回非0
#include
#include
int main()
{
int ret = system("ping -c 10 www.baidu.com");
if(ret!=0)
{
perror("system");
return -1;
}
return 0;
}
主题:ps -ef查看子进程
system函数用到的系统调用为fork、execve和waitpid
fork函数
a. 可以复制当前父进程,从而生成一个子进程。子进程的资源与父进程相同。缓冲区,socket,文件描述符等
b. 返回值:0代表子进程,-1代表失败,>0代表子进程pid
c. #include
对于fork来说,子进程与父进程的运行顺序是不一定的。
#include
#include
#include
#include
#include
int main()
{
int fd = open("test.txt",O_WRONLY);
pid_t pid = fork();
if(pid==-1)
{
perror("fork");
return -1;
}
else if(pid==0)
{ //
char buf[1]={'a'};
for(int i=0;i<10;i++)
{
write(fd,buf,sizeof(buf));
buf[0]++;
}
}
else
{
char buf[1]="1";
for(int i=0;i<10;i++)
{
write(fd,buf,sizeof(buf));
buf[0]++;
}
}
return 0;
}
对于这段代码来说,我们写入文件的顺序是随机的,也就是说是数字和字母混着写的,这代表着,当我们使用fork复制父进程生成子进程的时候,我们会复制一份与父进程相同的文件描述符表,但是他们共用一份文件描述struct file,也就是说,对于父子进程来说,文件的偏移量等信息是相同的。
#include
#include
#include
#include
#include
int main()
{
pid_t pid = fork();
int fd = open("test.txt",O_WRONLY);
if(pid==-1)
{
perror("fork");
return -1;
}
else if(pid==0)
{ //
char buf[1]="1";
write(fd,buf,sizeof(buf));
}
else
{
char buf[1]={'a'};
write(fd,buf,sizeof(buf));
}
return 0;
}
这段代码的结果是在文件中写入1或者a,这代表着在两个子进程中,我们通过open打开的文件描述,有着各自的文件描述符表以及文件描述struct file。并且对于一个文件来说,它可以同时被两个子进程打开并且进行修改。
execve函数
exec系列函数可以在同一个进程中跳转执行另外一个程序。
函数参数:
● char *__path:绝对路径
● char const argv[]:程序的参数列表
○ (1) 需要执行的程序命令(同__path)
○ (2) 执行程序需要传入的参数
○ (3) 最后一个参数必须是NULL
● char *const __envp[]:指向字符串数组的指针需要传入多个环境变量参数
○ (1) 环境变量参数固定格式 key=value
○ (2) 最后一个参数必须是NULL
#include
#include "unistd.h"
#include "sys/types.h"
#include "fcntl.h"
int main()
{
printf("我是父进程,pid是%d\n",getpid());
char *const argv[] = {"/home/wangze/桌面/Unix/第三章/process_del","我是参数",NULL};
char *const __envp[] = {"PATH=/home/wangze/bin:/home/wangze/.local/bin:/home/wangze/bin:/home/wangze/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin",NULL};
int ret = execve(argv[0],argv,__envp);
if(ret==-1)
{
perror("execve");
}
return 0;
}
#include
#include
#include
#include
#include
int main(int argc, char const *argv[])
{
printf("我是子进程,接收到的参数是%s,pid是%d\n",argv[1],getpid());
return 0;
}
execve函数经常与fork函数连用,使用场景如下,我们可以先使用fork生成一个子线程,父进程继续处理自己的逻辑,子线程如果满足某种条件,则可以立刻结束并且调用一个新的进程。
fork......
if 父进程:
继续执行
else if 子进程:
调用新的进程execve
waitpid
Linux中父进程除了可以启动子进程,还要负责回收子进程的状态。如果子进程结束后父进程没有正常回收,那么子进程就会变成一个僵尸进程——即程序执行完成,但是进程没有完全结束,其内核中PCB结构体没有释放。
waitpid这个函数可以让父进程等待子进程运行结束后再执行父进程。
#include
pid_t waitpid(pid_t pid,int *wstatus, int options);
● pid:等待模式
○ -1:等待任何子进程终止,并返回最先终止的子进程的ID
○ 0:儿子子进程的终止
○ 大于0:等待指定pid的进程终止
● wstatus:证书指针,子进程返回的状态码会保存到该int
● options:
○ WNOHANG :如果没有子进程终止,也立刻返回,通常用于查看子进程状态,而非等待
○ WUNTRACED:收到子进程处于收到信号停止的状态也返回
通常使用:waitpid(pid,&subprocess_status,0);
pstree -p 查看进程树
孤儿进程
父进程先于子进程结束,子进程就成为了孤儿进程。
孤儿进程会被其祖先自动领养。此时的子进程因为和终端切断了联系,所以很难再进行标准输入使其停止了,所以写代码的时候一定要注意避免出现孤儿进程。
匿名管道Pipe
● #include
● int pipe(int pipefd[2]);
pipe函数用于在内核空间创建管道,用于父子进程或者其他相关联的进程之间通过管道进行双向的数据传输。
pipefd: 用于返回指向管道两端的两个文件描述符。pipefd[0]指向管道的读端。pipefd[1]指向管道的写端。
● return
○ 成功:0
○ 不成功:-1,并且pipefd不会改变
常用宏定义
#define EXIT_FAILURE 1 /* Failing exit status. */
#define EXIT_SUCCESS 0 /* Successful exit status. */
这些退出状态可以直接作为_exit()函数的参数。
#define STDIN_FILENO 0 /* Standard input. */
#define STDOUT_FILENO 1 /* Standard output. */
#define STDERR_FILENO 2 /* Standard error output. */
#include
#include
#include
#include
#include
#include
int main()
{
int pipe_fd[2]={0};
__pid_t cpid;
int ret = pipe(pipe_fd);
if(ret==-1)
{
perror("pipe:");
}
cpid = fork();
if(cpid<0)
{
perror("fork:");
}
else if(cpid==0)
{
// 子进程读取
close(pipe_fd[1]); // close write
char buf[128]={0};
printf("children is running\n");
while(read(pipe_fd[0],buf,sizeof(buf))>0)
{
write(STDOUT_FILENO,buf,strlen(buf));
}
printf("\n");
close(pipe_fd[0]);
_exit(EXIT_SUCCESS);
}
else
{
// 父进程写入
close(pipe_fd[0]);
char buf[128]="this is father send data";
write(pipe_fd[1],buf, strlen(buf));
close(pipe_fd[1]); // close read
waitpid(cpid,NULL,0);
exit(EXIT_SUCCESS);
}
return 0;
}
注意:
● 对于子进程的退出,要使用_exit()函数,对于父进程的退出,要使用exit()函数
● 必须在fork之前进行pipe,否则会导致失败。
● pipe修改的pipe_fd,pipe_fd[0]代表读,pipe_fd[1]代表写。
● 在使用的使用,一端只能够使用一个文件描述符,也就是说,pipe_fd在一端只能读或者只能写,另一个必须关闭。
● 在使用完后要关闭使用的文件描述符
● 父进程在write之后立即关闭了写端,这会触发子进程read返回0
● 管道返回的两个文件描述符分别表示读写,各自指向一个struct file结构体,然而,它们并不对应真正的文件。struct file的私有数据指针属性private_data指向struct pipe_inode_info类型的结构体。
问题:
我们已经知道,read函数会直接读取文件中的内容,并且返回读取到的字节数,如果文件为空则立刻返回0。
● 如果我们在父进程中sleep(1),然后再执行代码会发生什么?
else
{
sleep(1);
// 父进程写入
close(pipe_fd[0]);
char buf[128]="this is father send data";
write(pipe_fd[1],buf, strlen(buf));
close(pipe_fd[1]); // close read
waitpid(cpid,NULL,0);
exit(EXIT_SUCCESS);
}
其实对于上面的问题,该段代码都可以正常执行,这是为什么呢?在这段代码中,显然,子进程read的时候,父进程还没有将数据放到管道中,因此read应该立刻返回0,并且不再打印buf的内容,但是为什么子进程正常打印呢?
对于read来说,如果read读取的是普通的文件,当文件为空的时候,read会立刻返回0,但是如果read读取的是管道,或者套接字这种类型的文件,read则会阻塞,直到有数据可以读取之后,再进行返回。因此代码可以正常执行。
● 如果 while(read(pipe_fd[0],buf,sizeof(buf))>0)这行代码不使用while会发生什么?
使用管道的限制:
(1)两个进程通过一个管道只能实现单向通信,比如上面的例子,父进程写子进程读,如果有时候也需要子进程写父进程读,就必须另开一个管道。
(2)管道的读写端通过打开的文件描述符来传递,因此要通信的两个进程必须从它们的公共祖先那里继承管道文件描述符。上面的例子是父进程把文件描述符传给子进程之后父子进程之间通信,也可以父进程fork两次,把文件描述符传给两个子进程,然后两个子进程之间通信,总之需要通过fork传递文件描述符使两个进程都能访问同一管道,它们才能通信。
有名管道FIFO
对于匿名管道来说,必须要是父子进程之间才能够进行通信,但是对于有名管道来说,任何进程之间都可以进行通信。但要注意的是,无论是有名管道还是匿名管道,同一条管道只应用于单向通信。
● #include
● int mkfifo(const char *pathname, mode_t mode);
● 返回值:成功返回0,失败返回-1
用于创建有名管道。该函数可以创建一个路径为pathname的FIFO专用文件,mode指定了FIFO的权限,FIFO的权限和它绑定的文件是一致的。FIFO和pipe唯一的区别在于创建方式的差异。一旦创建了FIFO专用文件,任何进程都可以像操作文件一样打开FIFO,执行读写操作。
#include
#include
#include
#include
#include
#include
#include
// send port
int main()
{
char *path = "/tmp/myfifo";
if(0!=mkfifo(path,0664))
{
perror("mkfifo:");
if(errno!=17) //为了防止因为重复创建mkfifo而无法执行函数
{
exit(EXIT_FAILURE);
}
}
pid_t cpit;
ssize_t readBytes;
char buf[128]={0};
int mkfifofd = open(path,O_WRONLY);
if(mkfifofd<0)
{
perror("mkfifofd");
exit(EXIT_FAILURE);
}
// STDIN have data then
while((readBytes = read(STDIN_FILENO,buf,sizeof(buf)))>0)
{
write(mkfifofd,buf,readBytes);
}
if(readBytes<0)
{
perror("read");
exit(EXIT_FAILURE);
}
printf("发送管道退出,进程终止\n");
close(mkfifofd);
// 关于unlik函数,看下面的注解
if(unlink(pipe_path) == -1) {
perror("fifo_write unlink");
}
return 0;
}
#include
#include
#include
#include
#include
#include
#include
// send port
int main()
{
char *path = "/tmp/myfifo";
pid_t cpit;
ssize_t readBytes;
char buf[128]={0};
int mkfifofd = open(path,O_RDONLY);
if(mkfifofd<0)
{
perror("mkfifofd");
exit(EXIT_FAILURE);
}
// STDIN have data then
while((readBytes = read(mkfifofd,buf,sizeof(buf)))>0)
{
write(STDOUT_FILENO,buf,readBytes);
}
if(readBytes<0)
{
perror("read");
exit(EXIT_FAILURE);
}
close(mkfifofd);
return 0;
}
有名管道使用完成后,应该通过unlink调用清除相关资源。这个函数只能调用一次,重复清除会提示No such file or directory
● #include
● int unlink(const char*pathname);
● return:成功返回0,失败返回-1,并设置errno
从文件系统中清除一个名称及其链接的文件
注意:
● /tmp/myfifo,这实际上就是fifo专用文件在文件系统的路径
● 打开有名管道时,flags只应为O_WRONLY或O_RDONLY。
● 内核为每个被进程打开的FIFO专用文件维护一个管道对象。当进程通过FIFO交换数据时,内核会在内部传递所有数据,不会将其写入文件系统。因此,/tmp/myfifo文件大小始终为0。
● 文件详细信息最开头的字母p表示这个是一个有名管道文件。
共享内存
首先,我们要学习两个函数shm_open() shm_unlink()。
shm_open()
shm_open可以开启一块内存共享对象,我们可以像使用一般文件描述符一样使用这块内存。
● #include
● int shm_open(const char *name, int oflag, mode_t mode);
● 返回值:成功执行返回一个文件描述符,失败返回-1
oflag的模式:O_CREATO_EXCLO_RDONLYO_RDWRO_TRUNC
● O_CREATO_EXCL这两个通常合并使用,用来表示如果共享内存对象已经存则,则返回错误(避免覆盖)
● O_TRUNC用于阶段现有对象至0,只有当打开模式中有O_RDWR的时候才有效。
shm_unlik()
shm_unlik用来删除一个先前由shm_open创建的共享内存对象,这个函数仅仅是移除了与共享内存对象关联的名称,使得无法通过该名称打开共享内存。只有当所有打开该共享内存的进程关闭他们的描述符后,系统才会真正的释放内存资源。
● int shm_unlik(const char *name);
● 返回值:成功返回0,失败返回-1;
truncate()和ftruncate()
这两个函数都可以将文件缩放到指定大小,如果缩小,则截断数据,如果放大,则扩展部分为\0,缩放前后文件的偏移量不会更改
● #include
● int ftruncate(int fd, off_t length);
● int truncate(const char *path, off_t length);
● 返回值:成功返回0,失败返回-1
不同的是,前者需要指定路径,后者需要提供文件描述符。ftruncate的对象可以是shm_open开启的内存对象,truncate缩放的文件必须是文件系统已存在的文件,否则会失败。
mmap()
mmap可以将一组设备或者文件映射到内存地址,我们在内存中寻址,就相当于在读取这个文件指定地址的数据,父进程在创建一个内存共享对象并且将其映射到内存区域后,子进程就可以正常读写该内存区,并且父进程也能看到更改。
● #include
● void *mmap(void *addr,size_t length,int prot,int flags,int fd,off_t offset);
○ addr:通常为NULL,让系统自动选择合适的起始地址
○ length:映射的内存区域长度,以字节为单位
○ prot:内存映射区域的保护标志,PROT_READPROT_WRITEPROT_EXECPROT_NONE
○ flags:MAP_SHAREDMAP_PRIVATEMAP_ANONYMOUSMAP_FIXED,一般使用第一个
○ fd:指定要映射的文件或设备
○ offset:从文件开头的偏移量,映射开始的位置
● 返回值:void* 成功的时候则返回映射区域的起始地址,失败则返回(void*)-1,并设置errno
int munmap(void *addr, size_t length);
用来取消映射的地址,addr是mmap的返回值,length必须和mmap的地址长度大小相同。
#include
#include
#include
#include
#include
#include
#include
int main()
{
char *share;
int fd;
pid_t pid;
char *shmName = "/letter";
fd = shm_open("/letter", O_CREAT | O_RDWR, 0644);
if(fd<0)
{
perror("shm_open");
exit(EXIT_FAILURE);
}
ftruncate(fd,100);
share = mmap(NULL,100,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if(share==MAP_FAILED)
{
perror("mmap");
exit(EXIT_FAILURE);
}
close(fd);
pid = fork();
if(pid<0)
{
perror("fork:");
exit(EXIT_FAILURE);
}
else if(pid==0)
{
// child
strcpy(share,"data\n");
}
else
{
// father
sleep(1);
printf("%s",share);
wait(NULL);
int ret = munmap(share,100);
if(ret==-1)
{
perror("munmap");
exit(EXIT_FAILURE);
}
}
shm_unlink(shmName);
return 0;
}
gcc 的时候要链接 -lrt
分步骤来说的话:
临时文件系统:
Linux的临时文件系统(tmpfs)是一种基于内存的文件系统,它将数据存储在RAM或者在需要时部分使用交换空间(swap)。tmpfs访问速度快,但因为存储在内存,重启后数据清空,通常用于存储一些临时文件。
我们可以通过df -h查看当前操作系统已挂载的文件系统。
● 内存共享对象在临时文件系统中的表示位于/dev/shm目录下。
消息队列
消息队列也是进程间的一种通信方式。
相关数据类型:mqd_t,struct mq_attr,struct timespec
#include
#include
#include
#include
#include
#include
// producer
int main()
{
char *path = "/letter_queue";
struct mq_attr attr;
attr.mq_flags = 0;
attr.mq_maxmsg = 10;
attr.mq_msgsize = 100;
attr.mq_curmsgs = 0;
mqd_t fd = mq_open(path,O_CREAT|O_WRONLY,0664,&attr);
if(fd<0)
{
perror("mq_open");
exit(EXIT_FAILURE);
}
char buf[100];
while(1)
{
memset(buf,0,sizeof(buf));
struct timespec timeinfo;
clock_gettime(CLOCK_REALTIME, &timeinfo);
timeinfo.tv_sec+=5;
int read_count = read(STDIN_FILENO,buf,sizeof(buf));
if(read_count==-1)
{
perror("read");
exit(EXIT_FAILURE);
}
else if(read_count==0)
{
char ch = EOF;
printf("send EOF\n");
if(-1==mq_timedsend(fd,&ch,sizeof(ch),0,&timeinfo))
{
perror("mq_timedsend");
}
break;
}
else
{
if(-1==mq_timedsend(fd,buf,sizeof(buf),0,&timeinfo))
{
perror("mq_timedsend");
}
printf("data send succeed\n");
}
}
close(fd);
return 0;
}
#include
#include
#include
#include
#include
#include
int main()
{
char *path = "/letter_queue";
struct mq_attr attr;
attr.mq_flags = 0;
attr.mq_maxmsg = 10;
attr.mq_msgsize = 100;
attr.mq_curmsgs = 0;
mqd_t fd = mq_open(path,O_RDONLY|O_CREAT,0664,&attr);
if(fd==-1)
{
perror("mq_open");
exit(EXIT_FAILURE);
}
while(1)
{
char buf[100];
struct timespec time_info;
clock_gettime(CLOCK_REALTIME, &time_info);
time_info.tv_sec += 86400;
memset(buf,0,sizeof(buf));
if(mq_timedreceive(fd,buf,sizeof(buf),NULL,&time_info)==-1)
{
perror("mq_timdreceive");
exit(EXIT_FAILURE);
}
else{
if(buf[0]==EOF)
{
printf("send is end");
break;
}
printf("recv data is: %s",buf);
}
}
close(fd);
if(-1==mq_unlink(path))
{
perror("mq_unlink");
exit(EXIT_FAILURE);
}
return 0;
}
● 注意:
○ mq_unlink函数只能使用一次,我们选择在接受代码中使用,因为如果接受代码退出了,那么代表发送终端已经按下了ctrl+D,因此我们此时可以解除mq_unlink的链接的。
○ clock_gettime(CLOCK_REALTIME, &time_info);这行代码可以获取当前时间,并且保存到time_info中,CLOCK_REALTIME是一个time.h中带的宏
○ 当终端按下Ctrl+D的时候,代表结束从屏幕的输入流,此时read(stdin)将会返回0,因此就可以运行我们发送EOF的逻辑。
在这里,我们仅仅是简单的讲解一下信号的基础知识点
每种信号都有其特定的含义和行为,进程可以通过注册信号处理函数来捕获信号并执行相应的操作,例如终止进程、忽略信号或者执行特定的处理逻辑。如果想查看所有的Linux信号,请执行kill -l指令,会得到。
我们可以通过signal系统调用注册信号处理函数。
● #include
● sighandler_t signal(int signum, sighandler_t handler);
sighandler_t是函数指针,用来标识信号处理函数
#include
#include
#include
#include
void sigint_handler(int signum)
{
printf("\n recv data:%d\n",signum);
exit(signum);
}
int main()
{
if(signal(SIGINT,sigint_handler)==SIG_ERR)
{
perror("signal");
return 1;
}
while(1)
{
sleep(1);
printf("now i am wait\n");
}
return 0;
}
当我们在运行代码后,在终端按下ctrl c的之后,就可以实现向程序发送信号。