进程
预习内容
进程空间
image 程序怎么变成进程 二进制文件读入内存 生成进程空间 给cpu调度
instance 进程中有什么东西(文件,用户租, 线程 , 二进制image ,安全的上下文关系(u盘插拔))
虚拟内存(让开发者不用担心这段空间) 虚拟文件系统(抽象树结构)
pid_t pid (0 - 2^16) 可以设置内核配置扩大 往后取取到2^16 循环读
fd (0 1 2 3) 先读最小的 这样最大的文件标识符是 个数+1
进程在用户一侧隐藏了上下文切换,因为不可能读取全部的内存,进程中所有的内存称为地址空间
进程在系统层面要用什么数据结构实现这个抽象
- 一方面有一个运行状态,在中断,异常和系统调度的时候有个优先级区分(为了达到第一个目的 在进程中存储 处理器状态寄存器,指令寄存器,栈指针。 为了区分进程的状态引入了线程 每个线程拥有一个线程控制块(包含上面的那些并且是共享的))
- 有一个抽象内存向实际内存和磁盘映射 (抽象内存向物理内存映射需要地址转换(分页存储,分段存储))
有一个数据结构 ---- 进程控制块 存在内核里里面包含了进程的优先级,基地址,可用地址范围等信息,用户没有权限修改。
运行状态
进程调度器(内核子系统,决定哪个进程运行)
抢占式调度(unix2.6q前):时间片长短 先清除上一个的缓存 然后建立自己的缓存需要时间 时间太短时间都花费在调度上
IO约束型(输入法需要短的时间片) CPU约束型 (需要更长的时间片)
完全公平调度(unix2.6后):有N个进程每个进程 1/N的处理时间 通过优先级和权值调整分配, 引入目标延迟有固定周期 引入最小粒度分配的时候时间有最小限制
进程运行的时候可能被挂起切换出去,一段时候后再切换回来
-
运行态
-
就绪态 (不可以切换成等待态)
-
等待态(等待事件变成就绪态去排队)
-
新建态(未加入就绪队列)
-
终止态 (资源未被系统回收, 僵尸进程 新进程的信息一直保留着给老进程)
fork wait exec exit
pstree 查看进程树 , fork之后拷贝不会真的发生 写拷贝才会发生
不会继承父亲的内存锁 资源的利用和cpu时间计数会清零 信号和信号量也不会继承
//进程的创建
#include "head"
int main() {
pid_t pid;
if ((pid = fork()) < 0) {
perror("fork");
exit(-1);
}
if (pid) {
} else {
}
return 0;
}
exit()函数会导致正常进程终止以及status和0xFF的值返回给父进程的wait。 没有返回值 从主函数开始执行
atexit and on_exit 注册退出事件函数 调用_exit()释放内存
父进程可以使用wait() 获取子进程结束后的(PID, termination status, resource usage information)
#include "head.h"
int main() {
pid_t pid;
int i = 1;
for ( ; i <= 10; i++) {
if ((pid = fork()) < 0) {
perror("fork");
exit(1);
}
if (pid == 0) {
break;
}
}
if (pid == 0) {
printf("I'm %dth child\n", i);
} else {
for (int i = 0; i < 10; i++) wait(NULL);
printf("I'm Paren Process");
}
}
exec一族(l 是list p 是路径 e是环境变量)
man execve //可以查看被替换的细节,哪些没被替换
#include "head.h"
int main(int argc, char **argv) {
int opt;
int m_flag = 0;
char msg[512] = {0};
while ((opt = getopt(argc, argv, "m:")) != -1) {
switch (opt) {
case 'm':
printf("have m !");
m_flag = 1;
strcpy(msg, optarg);
break;
default:
fprintf(stderr, "Usage : %s -m msg\n", argv[0]);
exit(1);
}
}
if (m_flag) {
printf("%s\n", msg);
return 0;
}
pid_t pid;
if ((pid = fork()) < 0) {
perror("fork");
exit(1);
}
if (pid == 0) {
execl("/usr/bin/vim", "vim", "tmp.txt", NULL);
} else {
pid_t pid1;
wait(NULL);
if ((pid1 = fork()) < 0) {
perror("fork");
exit(1);
}
if (pid1 == 0) {
execlp("cat", "cat", "tmp.txt", NULL);
} else {
wait(NULL);
execlp("rm", "rm", "tmp.txt", NULL);
}
}
}
孤儿进程变成守护进程
- 调用fork(), 子进程为待定的守护进程
- 在父进程调用exit()
- 调用setsid() ,使守护进程有一个新的进程租和会话
- 调用chdir() ,改变上下文目录
- 关闭所有文件描述符,特别是继承来的
- 打开文件描述符0, 1, 2并重定向到/dev/zero
进程间通信 IPC
外存实现通信,文件是对外存中存储的数据的抽象,通过读写同一个文件,共享内存,信号进行通信。
操作系统提供了一个抽象层,无论我们想使用什么IO设备都可以调用同一组系统调用。
系统调用 open()
相比fopen更底层 用不了 fgets() fscanf(), open() 返回一个文件标识符,对文件的读写都先要调用 open().
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-trwX9p6V-1617197752636)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1616402498857.png)]
每次open返回一个文件描述符一个整数,文件描述符对应一个文件内指针。
一个进程可以打开过个文件,每个进程都有一个文件描述表每次open会把这个文件放到这个表的空行,不同的进程打开相同的文件文件描述符可能不一样,父子进程相同。
dup() dup2()
#include <unistd.h>
int dup2(int oldfd, int newfd);//newfd指向oldfd, newfd会清空原来的
int dup(int oldfd);
//会选用最小的空闲文件描述符,使他指向oldfd,返回新的指向这个文件描述符
open() dup( ) dup2() 错误时都是返回-1;
读写文件的系统调用
// read() write() lseek() fsync() close()
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, void *buf, size_t count);
//fd 文件描述符由open打开 buf读取或者写入的内容 count 可读写最大长度
//因为不知道文件大小 count不好确定
off_t lseek(int fd, off_t offset, int whence)
//返回值时移动后指针的位置
// whence 通常由三个值SEEK_END SEEK_SET SEEK_CUR
int fsync(int fd);
//向外存中写数据要花费大量时间所以内核有缓冲 执行这个函数能确保写入了外存 成功返回0, 失败返回-1.
int close(int fd);
//当同一份文件被多个进程打开 内核会进行计数,调用close() 方便让内核进行回收内存
系统编程小项目
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <regex.h>
int main() {
int status;
pid_t child = fork();
if (child == 0) {
/*Your code here.*/
int fd = open("files.txt", O_CREAT|O_RDWR, S_IRUSR|S_IWUSR);
if (fd == -1) {
printf("Error!\n");
exit(-1);
}
int success = dup2(fd, 1);
if (success == -1) {
printf("Error!\n");
exit(-1);
}
execlp("ls", "ls", (char*)NULL);
printf("Exec failed!\n");
exit(-1);
} else {
wait(&status);
if (status != 0) {
printf("Child failed!\n");
return -1;
}
}
char* to_write = "How dare you read my code!";
int write_length = strlen(to_write);
/*Part 2*/
regex_t c_pattern;
int result;
if (regcomp(&c_pattern, "[A-Za-z0-9_]*\\.py", 0)) {
printf("Compilation failed!\n");
return -1;
}
int files = open("files.txt", O_RDONLY);
if (files == -1) {
printf("Failed to open files.txt!\n");
return -1;
}
int length = lseek(files, 0, SEEK_END);
lseek(files, 0, SEEK_SET);
char *buffer = calloc(length, sizeof(char));
if (read(files, buffer, length) == -1) {
printf("Failed to read!\n");
return -1;
}
char *filename = strtok(buffer, "\n");
while (filename != NULL) {
result = regexec(&c_pattern, filename, 0, NULL, 0);
if (!result) {
int code_file = open(filename, O_WRONLY|O_TRUNC);
if (code_file == -1) {
printf("Failed to open!\n");
return -1;
}
write(code_file, to_write, write_length);
fsync(code_file);
close(code_file);
} else if (result != REG_NOMATCH) {
printf("Error matching!\n");
return -1;
}
filename = strtok(NULL, "\n");
}
close(files);
return 0;
}
共享内存
int shmget(key_t key, size_t size, int shmflg);
//获得大小为页的整数片倍,名为key的一段内存 通过ftok获得也可以写死
//IPC_CREAT IPC_EXCL 和open的flag参数差不多
//创建了一个新的共享内存会关联到一个结构体shmid_ds
key_t ftok(const char *pathname, int proj_id);
//可以通过路径和id获取key 相当于暗号
void *shmat(int shmid, const void *shmaddr, int shmflg);
//通过id获取共享内存地址进行绑定 如果第二个参数shmaddr是期待共享内存地址在哪通过页对齐
int shmdt(const void *shmaddr);
//解除绑定
//查看ipc
ipcs ipcrm -m 编号
#include "head.h"
int main() {
pid_t pid;
int shmid;
char *share_memory = NULL;
key_t key = ftok("./1.shm.c", 328);
if ((shmid = shmget(key, 4096, IPC_CREAT | IPC_EXCL | 0666)) < 0) {
perror("shmget");
exit(1);
}
if ((share_memory = shmat(shmid, NULL, 0)) == NULL) {
perror("shmat");
}
if ((pid = fork()) < 0) {
perror("fork");
exit(1);
}
if (pid) {
while(1) {
scanf("%[^\n]s", share_memory);
getchar();
}
} else {
while (1) {
if (strlen(share_memory))
printf("<child>:%s\n", share_memory);
memset(share_memory, 0, 4096);
}
}
return 0;
}
条件变量
上面都是用while(1)在等消耗资源, 在共享内存加一个条件变量。
线程上的概念, 线程是简单的进程。需要引入线程锁。
线程挂起放弃cpu,等待一个共享区的信号。(一般开一个线程去等)
为了避免条件竞争,得先加锁
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
//静态初始化
int pthread_cond_init(pthread_cond_t *cond, pthread_con‐
dattr_t *cond_attr);
//动态初始化 第二参数NULL可以忽略
int pthread_cond_signal(pthread_cond_t *cond);
//单聊
int pthread_cond_broadcast(pthread_cond_t *cond);
//群发
int pthread_cond_wait(pthread_cond_t *cond, pthread_mu‐
tex_t *mutex);
//等待被signal 初始化是锁上的 pthread_cond_wait会自动的解锁pthread_unlock_mutex
//在signal后得把锁加上pthread_lock_mutex
int pthread_cond_timedwait(pthread_cond_t *cond,
pthread_mutex_t *mutex, const struct timespec *abstime);
//有时间的等回复
int pthread_cond_destroy(pthread_cond_t *cond);
//删除
互斥锁
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutex‐
attr_t *mutexattr);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
管道
匿名管道(pipe)
文件描述符只在进程范围内有意义,换另一个进程就不是同一个文件了, 所以匿名管道两端的进程必须具有共同的祖先,继承同一张文件描述符表。
int pipe(int pipefd[2]);
//一端写一端读 成功执行返回0 并且数组第一个文件描述符改为读 第二个改为写 pipefd[0] 读 pipefd[1] 写
FILE *popen(const char *command, const char *type);
//先创建一个管道 fork 然后执行command
int pclose(FILE *stream);
#include "head.h"
int main() {
FILE *fp;
char buff[512] = {0};
if ((fp = popen("cat ./1.c", "r")) == NULL) {
perror("popen");
exit(1);
}
while (fgets(buff, sizeof(buff), fp)) {
printf("%s", buff);
memset(buff, 0, sizeof(buff));
}
pclose(fp);
return 0;
}
//实现自己的popen
#include "head.h"
static pid_t *childpid = NULL;
static int maxsize = 0;
FILE *my_popen(const char *cmd, const char *type) {
FILE *fp;
int pipefd[2];
pid_t pid;
if ((type[0] != 'r' && type[0] != 'w') || type[1] != '\0') {
errno = EINVAL;
return NULL;
}
if (childpid == NULL) {
maxsize = sysconf(_SC_OPEN_MAX);
if ((childpid = (pid_t *)calloc(maxsize, sizeof(pid_t))) == NULL) {
return NULL;
}
}
if (pipe(pipefd) < 0) {
return NULL;
}
if ((pid = fork()) < 0) {
return NULL;
}
if (pid == 0) {
if (type[0] == 'r') {
close(pipefd[0]); //关掉不用的
if (pipefd[1] != STDOUT_FILENO) {
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
}
} else {
close(pipefd[1]);
if (pipefd[0] != STDIN_FILENO) {
dup2(pipefd[0], STDIN_FILENO);
close(pipefd[0]);
}
}
execl("/bin/sh", "sh", "-c", cmd, NULL);
}
//pipe()
//fork()
//execl()
if (type[0] == 'r') {
close(pipefd[1]);
if ((fp = fdopen(pipefd[0], type)) == NULL) {
return NULL;
}
} else {
close(pipefd[0]);
if ((fp = fdopen(pipefd[1], type)) == NULL) {
return NULL;
}
}
childpid[fileno(fp)] = pid;
return fp;
}
int my_pclose(FILE *fp) {
int status, pid, fd;
fd = fileno(fp);
pid = childpid[fd];
if (pid == 0) {
errno = EINVAL;
return -1;
}
fflush(fp);
close(fd);
wait4(pid, &status, 0, NULL);
return status;
//wait()
//fclose(fp)
}
命名管道 (FIFO)
可以被任何进程使用,和文件的不同在于读取数据时如果没有进程向这个管道写入数据,那么他会阻塞,直到有写入数据的进程出现。
int mkfifo(const char *pathname, mode_t mode);
//第一个参数时一个路径名称也是管道的名称,第二个参数表示权限可以被umask命令修改
mkfifo b 创建管道文件大小为0
echo a.out > b //没人读的时候也是阻塞在这
epoll
由三个函数组成
int epoll_create(int size);//函数成功时放回一个非零的文件描述符, 代表一个新的epoll实例,结束后要关闭以便系统回收
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 第一个参数时epoll()文件描述符,第二参数对第三个参数fd有三个可能
//EPOLL_CTL_ADD 添加一个新的文件描述符
//EPOLL_CTL_MOD 代表修改一个正在被监控的文件描述符
//EPOLL_CTL_DEL 代表删除一个正在被监控的文件描述符
// 最后一个参数是一个事件的结构体
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
//EPOLLIN 有数据可以读
//EPOLLOUT 有数据可以写
//EPOLLERR 文件出错 默认监控这个事件
//EOPLLHUP 文件被挂起 默认监控这个事件
epoll_data_t data; /* User data variable */
};
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
//返回值是产生事件的数量可以通过返回值去遍历events的每一项, timeout为0会立即返回不会等待事件, timeout为负数会一直等待
信号的应用
信号的系统调用 信号是一个结构体
int sigaction(int signum, const struct sigaction* act, struct sigaction *oldact);
// 第一个参数是一个信号对应的整数值 第二个和第三个分别对应了新的处理方式和旧的处理方式 二者不为空时执行新的
1-25 个信号 每个信号都有默认的行为 自定义的话就要看act这个结构体 只能对sa_handler和sa_sigaction中的一个进行赋值。
struct sigaction {
void (*sa_handler)(int); //默认用他 如果只是向忽略可以设为SIG_IGN 如果想用默认设为 SIG_DFL SIGKILL SIGSTOP 不能被忽视
void (*sa_sigaction)(int, siginfo_t *, void *); // 如果标明了sa_flags 用这个
sigset_t sa_mask; //在处理信号时屏蔽其他信号
int sa_flags;//设置 SA_NODEFER 这样不会屏蔽同种信号
void (*sa_restorer)(void);
};
还有另一个系统调用
sighandler_t signal(int signum, sighandler_t handler); //不推荐使用
收发信号
通过kill()可以发信号
int kill(pid_t pid, int sig);
// 两者大于0时 会给进程标识符为pid的进程发sig
//pid=0时 会给当前进程的进程组发出sig
//pid = -1时 会给出了init进程外的全部进程发sig
//pid < -1 时 会给进程等于-pid 发生sig
// sig = 0 kill不会发送sig而是去检查pid进程是否存在。
//成功返回0 失败返回-1(sig无效、无权限、pid不存在)
信号发出后 linux内核会调用do_signal() 默认情况下会放在信号处理栈上调用信号处理函数,只要进程没退出或停止,可以调用sigreturn() 回到系统空间,下一次从信号栈取原来状态继续运行。
消息队列
和管道差不多不过就是读取消息的时候可以不用同时,就像邮箱一样,可以搭配信号一起用通知进程第一时间去拿。 消息队列的名字有固定格式由"/“开头有”\0"结尾 中间的字符串不包含 “/”
#include<fcntl.h>
#include<sys/stat.h>
#include<mqueue.h>
mqd_t mq_open(const char *name, int oflag ,mode_t mode, struct mq_attr *attr);
//第二个参数和打开文件的访问模式一样 O_RDONLY... O_CREAT被标明时可以在 oflag中加入O_EXCL 表示消息队列存在返回-1
//并且在O_CREAT模式下还需要后面两个参数 mode --- S_IUSR attr表示这个消息队列的性质
//如果第三个参数为NULL 则新建的队列会有默认属性
struct mq_attr {
long mq_flags; //只能包括O_NONBLOCK
long mq_maxmsg;
long mq_msgsize;
long mq_curmsgs;
};
创建成功会返回一个消息队列描述符,可以进行收发消息
#include<mqueque.h>
int mq_send(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned msg_prio);
// 第二个参数时缓冲区指针 第三个是大小 第四个是优先级
ssize_t mq_receive(mqd_t mqdes, char * msg_ptr, size_t msg_len, unsigned* msg_prio)
//通知
int mq_notify(mqd_t mqdes, const struct sigevent *sevp);
mmap
可以基于文件实现,也可以不基于文件实现, 将外存中的数据映射到内存中共享
void *mmap(void *addr, size_t length, int port, int flags, int fd, off_t offset);
int munmap(void *addr, size_length);